From ec5c3fc452e2b19632586a3b20c49fe945bb019c Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 16 Apr 2025 11:40:17 -0400 Subject: [PATCH] Refactor API endpoints and modularize controllers and models --- addUser.php | 86 -- auth.php | 270 ---- changePassword.php | 99 -- checkAuth.php | 70 - config.php => config/config.php | 11 +- copyFiles.php | 153 -- createFolder.php | 96 -- createFolderShareLink.php | 94 -- createShareLink.php | 75 - deleteFiles.php | 161 -- deleteFolder.php | 99 -- deleteTrashFiles.php | 104 -- download.php | 85 -- downloadSharedFile.php | 85 -- downloadZip.php | 133 -- extractZip.php | 165 -- getConfig.php | 46 - getFileList.php | 106 -- getFileTag.php | 40 - getFolderList.php | 97 -- getTrashItems.php | 68 - getUserPermissions.php | 47 - getUsers.php | 31 - login_basic.php | 120 -- logout.php | 50 - moveFiles.php | 164 -- public/api/addUser.php | 8 + public/api/admin/getConfig.php | 8 + public/api/admin/updateConfig.php | 8 + public/api/auth/auth.php | 9 + public/api/auth/checkAuth.php | 8 + public/api/auth/login_basic.php | 8 + public/api/auth/logout.php | 8 + public/api/auth/token.php | 8 + public/api/changePassword.php | 8 + public/api/file/copyFiles.php | 8 + public/api/file/createShareLink.php | 8 + public/api/file/deleteFiles.php | 8 + public/api/file/deleteTrashFiles.php | 8 + public/api/file/download.php | 8 + public/api/file/downloadZip.php | 8 + public/api/file/extractZip.php | 8 + public/api/file/getFileList.php | 8 + public/api/file/getFileTag.php | 8 + public/api/file/getTrashItems.php | 8 + public/api/file/moveFiles.php | 8 + public/api/file/renameFile.php | 8 + public/api/file/restoreFiles.php | 8 + public/api/file/saveFile.php | 8 + public/api/file/saveFileTag.php | 8 + public/api/file/share.php | 8 + public/api/file/symlink | 2 + public/api/folder/createFolder.php | 8 + public/api/folder/createShareFolderLink.php | 8 + public/api/folder/deleteFolder.php | 8 + public/api/folder/downloadSharedFile.php | 8 + public/api/folder/getFolderList.php | 8 + public/api/folder/renameFolder.php | 8 + public/api/folder/shareFolder.php | 8 + public/api/folder/uploadToSharedFolder.php | 8 + public/api/getUserPermissions.php | 8 + public/api/getUsers.php | 8 + public/api/removeUser.php | 8 + public/api/totp_disable.php | 9 + public/api/totp_recover.php | 8 + public/api/totp_saveCode.php | 8 + public/api/totp_setup.php | 9 + public/api/totp_verify.php | 9 + public/api/updateUserPanel.php | 8 + public/api/updateUserPermissions.php | 8 + public/api/upload/removeChunks.php | 8 + public/api/upload/upload.php | 7 + {assets => public/assets}/favicon.ico | Bin {assets => public/assets}/logo.png | Bin {assets => public/assets}/logo.svg | Bin {css => public/css}/styles.css | 0 public/index.html | 485 ++++++ {js => public/js}/auth.js | 35 +- {js => public/js}/authModals.js | 24 +- {js => public/js}/domUtils.js | 0 {js => public/js}/dragAndDrop.js | 0 {js => public/js}/fileActions.js | 18 +- {js => public/js}/fileDragDrop.js | 2 +- {js => public/js}/fileEditor.js | 2 +- {js => public/js}/fileListView.js | 2 +- {js => public/js}/fileManager.js | 0 {js => public/js}/fileMenu.js | 0 {js => public/js}/filePreview.js | 4 +- {js => public/js}/fileTags.js | 6 +- {js => public/js}/folderManager.js | 14 +- {js => public/js}/folderShareModal.js | 2 +- {js => public/js}/i18n.js | 0 {js => public/js}/main.js | 2 +- {js => public/js}/networkUtils.js | 0 {js => public/js}/trashRestoreDelete.js | 14 +- {js => public/js}/upload.js | 6 +- removeChunks.php | 53 - removeUser.php | 88 -- renameFile.php | 114 -- renameFolder.php | 98 -- restoreFiles.php | 175 --- saveFile.php | 121 -- saveFileTag.php | 148 -- share.php | 149 -- shareFolder.php | 457 ------ src/controllers/adminController.php | 210 +++ src/controllers/authController.php | 524 +++++++ src/controllers/fileController.php | 1513 +++++++++++++++++++ src/controllers/folderController.php | 907 +++++++++++ src/controllers/uploadController.php | 177 +++ src/controllers/userController.php | 950 ++++++++++++ src/models/AdminModel.php | 118 ++ src/models/AuthModel.php | 124 ++ src/models/FileModel.php | 1249 +++++++++++++++ src/models/FolderModel.php | 570 +++++++ src/models/UploadModel.php | 266 ++++ src/models/UserModel.php | 657 ++++++++ token.php | 8 - totp_disable.php | 74 - totp_recover.php | 115 -- totp_saveCode.php | 85 -- totp_setup.php | 167 -- totp_verify.php | 157 -- updateConfig.php | 99 -- updateUserPanel.php | 80 - updateUserPermissions.php | 94 -- upload.php | 273 ---- uploadToSharedFolder.php | 151 -- 128 files changed, 8189 insertions(+), 5318 deletions(-) delete mode 100644 addUser.php delete mode 100644 auth.php delete mode 100644 changePassword.php delete mode 100644 checkAuth.php rename config.php => config/config.php (94%) delete mode 100644 copyFiles.php delete mode 100644 createFolder.php delete mode 100644 createFolderShareLink.php delete mode 100644 createShareLink.php delete mode 100644 deleteFiles.php delete mode 100644 deleteFolder.php delete mode 100644 deleteTrashFiles.php delete mode 100644 download.php delete mode 100644 downloadSharedFile.php delete mode 100644 downloadZip.php delete mode 100644 extractZip.php delete mode 100644 getConfig.php delete mode 100644 getFileList.php delete mode 100644 getFileTag.php delete mode 100644 getFolderList.php delete mode 100644 getTrashItems.php delete mode 100644 getUserPermissions.php delete mode 100644 getUsers.php delete mode 100644 login_basic.php delete mode 100644 logout.php delete mode 100644 moveFiles.php create mode 100644 public/api/addUser.php create mode 100644 public/api/admin/getConfig.php create mode 100644 public/api/admin/updateConfig.php create mode 100644 public/api/auth/auth.php create mode 100644 public/api/auth/checkAuth.php create mode 100644 public/api/auth/login_basic.php create mode 100644 public/api/auth/logout.php create mode 100644 public/api/auth/token.php create mode 100644 public/api/changePassword.php create mode 100644 public/api/file/copyFiles.php create mode 100644 public/api/file/createShareLink.php create mode 100644 public/api/file/deleteFiles.php create mode 100644 public/api/file/deleteTrashFiles.php create mode 100644 public/api/file/download.php create mode 100644 public/api/file/downloadZip.php create mode 100644 public/api/file/extractZip.php create mode 100644 public/api/file/getFileList.php create mode 100644 public/api/file/getFileTag.php create mode 100644 public/api/file/getTrashItems.php create mode 100644 public/api/file/moveFiles.php create mode 100644 public/api/file/renameFile.php create mode 100644 public/api/file/restoreFiles.php create mode 100644 public/api/file/saveFile.php create mode 100644 public/api/file/saveFileTag.php create mode 100644 public/api/file/share.php create mode 100644 public/api/file/symlink create mode 100644 public/api/folder/createFolder.php create mode 100644 public/api/folder/createShareFolderLink.php create mode 100644 public/api/folder/deleteFolder.php create mode 100644 public/api/folder/downloadSharedFile.php create mode 100644 public/api/folder/getFolderList.php create mode 100644 public/api/folder/renameFolder.php create mode 100644 public/api/folder/shareFolder.php create mode 100644 public/api/folder/uploadToSharedFolder.php create mode 100644 public/api/getUserPermissions.php create mode 100644 public/api/getUsers.php create mode 100644 public/api/removeUser.php create mode 100644 public/api/totp_disable.php create mode 100644 public/api/totp_recover.php create mode 100644 public/api/totp_saveCode.php create mode 100644 public/api/totp_setup.php create mode 100644 public/api/totp_verify.php create mode 100644 public/api/updateUserPanel.php create mode 100644 public/api/updateUserPermissions.php create mode 100644 public/api/upload/removeChunks.php create mode 100644 public/api/upload/upload.php rename {assets => public/assets}/favicon.ico (100%) rename {assets => public/assets}/logo.png (100%) rename {assets => public/assets}/logo.svg (100%) rename {css => public/css}/styles.css (100%) create mode 100644 public/index.html rename {js => public/js}/auth.js (94%) rename {js => public/js}/authModals.js (98%) rename {js => public/js}/domUtils.js (100%) rename {js => public/js}/dragAndDrop.js (100%) rename {js => public/js}/fileActions.js (97%) rename {js => public/js}/fileDragDrop.js (99%) rename {js => public/js}/fileEditor.js (99%) rename {js => public/js}/fileListView.js (99%) rename {js => public/js}/fileManager.js (100%) rename {js => public/js}/fileMenu.js (100%) rename {js => public/js}/filePreview.js (99%) rename {js => public/js}/fileTags.js (99%) rename {js => public/js}/folderManager.js (98%) rename {js => public/js}/folderShareModal.js (98%) rename {js => public/js}/i18n.js (100%) rename {js => public/js}/main.js (99%) rename {js => public/js}/networkUtils.js (100%) rename {js => public/js}/trashRestoreDelete.js (96%) rename {js => public/js}/upload.js (99%) delete mode 100644 removeChunks.php delete mode 100644 removeUser.php delete mode 100644 renameFile.php delete mode 100644 renameFolder.php delete mode 100644 restoreFiles.php delete mode 100644 saveFile.php delete mode 100644 saveFileTag.php delete mode 100644 share.php delete mode 100644 shareFolder.php create mode 100644 src/controllers/adminController.php create mode 100644 src/controllers/authController.php create mode 100644 src/controllers/fileController.php create mode 100644 src/controllers/folderController.php create mode 100644 src/controllers/uploadController.php create mode 100644 src/controllers/userController.php create mode 100644 src/models/AdminModel.php create mode 100644 src/models/AuthModel.php create mode 100644 src/models/FileModel.php create mode 100644 src/models/FolderModel.php create mode 100644 src/models/UploadModel.php create mode 100644 src/models/UserModel.php delete mode 100644 token.php delete mode 100644 totp_disable.php delete mode 100644 totp_recover.php delete mode 100644 totp_saveCode.php delete mode 100644 totp_setup.php delete mode 100644 totp_verify.php delete mode 100644 updateConfig.php delete mode 100644 updateUserPanel.php delete mode 100644 updateUserPermissions.php delete mode 100644 upload.php delete mode 100644 uploadToSharedFolder.php diff --git a/addUser.php b/addUser.php deleted file mode 100644 index f90222b..0000000 --- a/addUser.php +++ /dev/null @@ -1,86 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; - } - if ( - !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || - !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true - ) { - echo json_encode(["error" => "Unauthorized"]); - exit; - } -} - -// Get input data from JSON. -$data = json_decode(file_get_contents("php://input"), true); -$newUsername = trim($data["username"] ?? ""); -$newPassword = trim($data["password"] ?? ""); - -// In setup mode, force the new user to be admin. -if ($setupMode) { - $isAdmin = "1"; -} else { - $isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user. -} - -// Validate input. -if (!$newUsername || !$newPassword) { - echo json_encode(["error" => "Username and password required"]); - exit; -} - -// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces). -if (!preg_match(REGEX_USER, $newUsername)) { - echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]); - exit; -} - -// Ensure users.txt exists. -if (!file_exists($usersFile)) { - file_put_contents($usersFile, ''); -} - -// Check if username already exists. -$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); -foreach ($existingUsers as $line) { - list($storedUser, $storedHash, $storedRole) = explode(':', trim($line)); - if ($newUsername === $storedUser) { - echo json_encode(["error" => "User already exists"]); - exit; - } -} - -// Hash the password. -$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); - -// Prepare new user line. -$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL; - -// In setup mode, overwrite users.txt; otherwise, append to it. -if ($setupMode) { - file_put_contents($usersFile, $newUserLine); -} else { - file_put_contents($usersFile, $newUserLine, FILE_APPEND); -} - -echo json_encode(["success" => "User added successfully"]); -?> \ No newline at end of file diff --git a/auth.php b/auth.php deleted file mode 100644 index fce81a0..0000000 --- a/auth.php +++ /dev/null @@ -1,270 +0,0 @@ -getMessage()); - http_response_code(500); - echo json_encode(["error" => "Internal Server Error"]); - exit(); -}); - -/** - * Helper: Get the user's role from users.txt. - */ -function getUserRole($username) { - $usersFile = USERS_DIR . USERS_FILE; - if (file_exists($usersFile)) { - foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { - $parts = explode(":", trim($line)); - if (count($parts) >= 3 && $parts[0] === $username) { - return trim($parts[2]); - } - } - } - return null; -} - -/* --- OIDC Authentication Flow --- */ -// Detect either ?oidc=… or a callback that only has ?code= -$oidcAction = $_GET['oidc'] ?? null; -if (!$oidcAction && isset($_GET['code'])) { - $oidcAction = 'callback'; -} -if ($oidcAction) { - $adminConfigFile = USERS_DIR . 'adminConfig.json'; - if (file_exists($adminConfigFile)) { - $enc = file_get_contents($adminConfigFile); - $dec = decryptData($enc, $encryptionKey); - $cfg = $dec !== false ? json_decode($dec, true) : []; - } else { - $cfg = []; - } - $oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com'; - $oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID'; - $oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET'; - // Use your production domain for redirect URI. - $oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/auth.php?oidc=callback'; - - $oidc = new Jumbojett\OpenIDConnectClient( - $oidc_provider_url, - $oidc_client_id, - $oidc_client_secret - ); - $oidc->setRedirectURL($oidc_redirect_uri); - - if ($oidcAction === 'callback') { - try { - $oidc->authenticate(); - $username = $oidc->requestUserInfo('preferred_username'); - - // Check if this user has a TOTP secret. - $usersFile = USERS_DIR . USERS_FILE; - $totp_secret = null; - if (file_exists($usersFile)) { - foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { - $parts = explode(":", trim($line)); - if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { - $totp_secret = decryptData($parts[3], $encryptionKey); - break; - } - } - } - if ($totp_secret) { - // Hold pending login & prompt for TOTP. - $_SESSION['pending_login_user'] = $username; - $_SESSION['pending_login_secret'] = $totp_secret; - header("Location: index.html?totp_required=1"); - exit(); - } - - // No TOTP → finalize login. - session_regenerate_id(true); - $_SESSION["authenticated"] = true; - $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = (getUserRole($username) === "1"); - $_SESSION["folderOnly"] = loadUserPermissions($username); - - header("Location: index.html"); - exit(); - } catch (Exception $e) { - error_log("OIDC authentication error: " . $e->getMessage()); - http_response_code(401); - echo json_encode(["error" => "Authentication failed."]); - exit(); - } - } else { - // Initiate OIDC authentication. - try { - $oidc->authenticate(); - exit(); - } catch (Exception $e) { - error_log("OIDC initiation error: " . $e->getMessage()); - http_response_code(401); - echo json_encode(["error" => "Authentication initiation failed."]); - exit(); - } - } -} - -/* --- Fallback: Form-based Authentication --- */ -$usersFile = USERS_DIR . USERS_FILE; -$maxAttempts = 5; -$lockoutTime = 30 * 60; // 30 minutes -$attemptsFile = USERS_DIR . 'failed_logins.json'; -$failedLogFile = USERS_DIR . 'failed_login.log'; -$persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; - -function loadFailedAttempts($file) { - if (file_exists($file)) { - $data = json_decode(file_get_contents($file), true); - if (is_array($data)) { - return $data; - } - } - return []; -} - -function saveFailedAttempts($file, $data) { - file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX); -} - -$ip = $_SERVER['REMOTE_ADDR']; -$currentTime = time(); -$failedAttempts = loadFailedAttempts($attemptsFile); - -if (isset($failedAttempts[$ip])) { - $attemptData = $failedAttempts[$ip]; - if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { - http_response_code(429); - echo json_encode(["error" => "Too many failed login attempts. Please try again later."]); - exit(); - } -} - -function authenticate($username, $password) { - global $usersFile, $encryptionKey; - if (!file_exists($usersFile)) { - return false; - } - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) < 3) continue; - if ($username === $parts[0] && password_verify($password, $parts[1])) { - $result = ['role' => $parts[2]]; - $result['totp_secret'] = (isset($parts[3]) && !empty($parts[3])) - ? decryptData($parts[3], $encryptionKey) - : null; - return $result; - } - } - return false; -} - -$data = json_decode(file_get_contents("php://input"), true); -$username = trim($data["username"] ?? ""); -$password = trim($data["password"] ?? ""); -$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true; - -if (!$username || !$password) { - http_response_code(400); - echo json_encode(["error" => "Username and password are required"]); - exit(); -} - -if (!preg_match(REGEX_USER, $username)) { - http_response_code(400); - echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]); - exit(); -} - -$user = authenticate($username, $password); -if ($user !== false) { - if (!empty($user['totp_secret'])) { - // If TOTP code is missing or malformed, indicate that TOTP is required. - if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) { - // ← STORE pending user & secret so recovery can see it - $_SESSION['pending_login_user'] = $username; - $_SESSION['pending_login_secret'] = $user['totp_secret']; - echo json_encode([ - "totp_required" => true, - "message" => "TOTP code required" - ]); - exit(); - } else { - $tfa = new \RobThree\Auth\TwoFactorAuth( - new GoogleChartsQrCodeProvider(), // QR code provider - 'FileRise', // issuer - 6, // number of digits - 30, // period in seconds - Algorithm::Sha1 // Correct enum case name from your enum - ); - $providedCode = trim($data['totp_code']); - if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) { - echo json_encode(["error" => "Invalid TOTP code"]); - exit(); - } - } - } - if (isset($failedAttempts[$ip])) { - unset($failedAttempts[$ip]); - saveFailedAttempts($attemptsFile, $failedAttempts); - } - session_regenerate_id(true); - $_SESSION["authenticated"] = true; - $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = ($user['role'] === "1"); - $_SESSION["folderOnly"] = loadUserPermissions($username); - - if ($rememberMe) { - $token = bin2hex(random_bytes(32)); - $expiry = time() + (30 * 24 * 60 * 60); - $persistentTokens = []; - if (file_exists($persistentTokensFile)) { - $encryptedContent = file_get_contents($persistentTokensFile); - $decryptedContent = decryptData($encryptedContent, $encryptionKey); - $persistentTokens = json_decode($decryptedContent, true); - if (!is_array($persistentTokens)) { - $persistentTokens = []; - } - } - $persistentTokens[$token] = [ - "username" => $username, - "expiry" => $expiry, - "isAdmin" => ($_SESSION["isAdmin"] === true) - ]; - $encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey); - file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX); - // Define $secure based on whether HTTPS is enabled - $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); - setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true); - } - - echo json_encode([ - "status" => "ok", - "success" => "Login successful", - "isAdmin" => $_SESSION["isAdmin"], - "folderOnly"=> $_SESSION["folderOnly"], - "username" => $_SESSION["username"] - ]); -} else { - if (isset($failedAttempts[$ip])) { - $failedAttempts[$ip]['count']++; - $failedAttempts[$ip]['last_attempt'] = $currentTime; - } else { - $failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime]; - } - saveFailedAttempts($attemptsFile, $failedAttempts); - $logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL; - file_put_contents($failedLogFile, $logLine, FILE_APPEND); - http_response_code(401); - echo json_encode(["error" => "Invalid credentials"]); -} -?> \ No newline at end of file diff --git a/changePassword.php b/changePassword.php deleted file mode 100644 index 945f426..0000000 --- a/changePassword.php +++ /dev/null @@ -1,99 +0,0 @@ - "Unauthorized"]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -if (!$username) { - echo json_encode(["error" => "No username in session"]); - exit; -} - -// CSRF token check. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(["error" => "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Get POST data. -$data = json_decode(file_get_contents("php://input"), true); -$oldPassword = trim($data["oldPassword"] ?? ""); -$newPassword = trim($data["newPassword"] ?? ""); -$confirmPassword = trim($data["confirmPassword"] ?? ""); - -// Validate input. -if (!$oldPassword || !$newPassword || !$confirmPassword) { - echo json_encode(["error" => "All fields are required."]); - exit; -} -if ($newPassword !== $confirmPassword) { - echo json_encode(["error" => "New passwords do not match."]); - exit; -} - -// Path to users file. -$usersFile = USERS_DIR . USERS_FILE; -if (!file_exists($usersFile)) { - echo json_encode(["error" => "Users file not found"]); - exit; -} - -// Read current users. -$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); -$userFound = false; -$newLines = []; - -foreach ($lines as $line) { - $parts = explode(':', trim($line)); - // Expect at least 3 parts: username, hashed password, and role. - if (count($parts) < 3) { - // Skip invalid lines. - $newLines[] = $line; - continue; - } - $storedUser = $parts[0]; - $storedHash = $parts[1]; - $storedRole = $parts[2]; - // Preserve TOTP secret if it exists. - $totpSecret = (count($parts) >= 4) ? $parts[3] : ""; - - if ($storedUser === $username) { - $userFound = true; - // Verify the old password. - if (!password_verify($oldPassword, $storedHash)) { - echo json_encode(["error" => "Old password is incorrect."]); - exit; - } - // Hash the new password. - $newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); - // Rebuild the line with the new hash and preserve TOTP secret if present. - if ($totpSecret !== "") { - $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret; - } else { - $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole; - } - } else { - $newLines[] = $line; - } -} - -if (!$userFound) { - echo json_encode(["error" => "User not found."]); - exit; -} - -// Save updated users file. -if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) { - echo json_encode(["success" => "Password updated successfully."]); -} else { - echo json_encode(["error" => "Could not update password."]); -} -?> \ No newline at end of file diff --git a/checkAuth.php b/checkAuth.php deleted file mode 100644 index a324048..0000000 --- a/checkAuth.php +++ /dev/null @@ -1,70 +0,0 @@ - true]); - exit(); -} - -// Check session authentication. -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["authenticated" => false]); - exit(); -} - -/** - * Helper function to get a user's role from users.txt. - * Returns the role as a string (e.g. "1") or null if not found. - */ -function getUserRole($username) { - global $usersFile; - if (file_exists($usersFile)) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(":", trim($line)); - if (count($parts) >= 3 && $parts[0] === $username) { - return trim($parts[2]); - } - } - } - return null; -} - -// Determine if TOTP is enabled by checking users.txt. -$totp_enabled = false; -$username = $_SESSION['username'] ?? ''; -if ($username) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(":", trim($line)); - // Assuming first field is username and fourth (if exists) is the TOTP secret. - if ($parts[0] === $username) { - if (isset($parts[3]) && trim($parts[3]) !== "") { - $totp_enabled = true; - } - break; - } - } -} - -// Use getUserRole() to determine admin status. -// We cast the role to an integer so that "1" (string) is treated as true. -$userRole = getUserRole($username); -$isAdmin = ((int)$userRole === 1); - -// Build and return the JSON response. -$response = [ - "authenticated" => true, - "isAdmin" => $isAdmin, - "totp_enabled" => $totp_enabled, - "username" => $username, - "folderOnly" => isset($_SESSION["folderOnly"]) ? $_SESSION["folderOnly"] : false -]; - -echo json_encode($response); -?> \ No newline at end of file diff --git a/config.php b/config/config.php similarity index 94% rename from config.php rename to config/config.php index 44544e8..dda052f 100644 --- a/config.php +++ b/config/config.php @@ -17,6 +17,7 @@ header("Permissions-Policy: geolocation=(), microphone=(), camera=()"); header("X-XSS-Protection: 1; mode=block"); // Define constants. +define('PROJECT_ROOT', dirname(__DIR__)); define('UPLOAD_DIR', '/var/www/uploads/'); define('USERS_DIR', '/var/www/users/'); define('USERS_FILE', 'users.txt'); @@ -116,7 +117,11 @@ $cookieParams = [ 'httponly' => true, 'samesite' => 'Lax' ]; - +// At the very beginning of config.php +ini_set('session.save_path', __DIR__ . '/../sessions'); +if (!is_dir(__DIR__ . '/../sessions')) { + mkdir(__DIR__ . '/../sessions', 0777, true); +} if (session_status() === PHP_SESSION_NONE) { session_set_cookie_params($cookieParams); ini_set('session.gc_maxlifetime', 7200); @@ -160,8 +165,8 @@ define('BASE_URL', 'http://yourwebsite/uploads/'); if (strpos(BASE_URL, 'yourwebsite') !== false) { $defaultShareUrl = isset($_SERVER['HTTP_HOST']) ? "http://" . $_SERVER['HTTP_HOST'] . "/share.php" - : "http://localhost/share.php"; + : "http://localhost/public/api/file/share.php"; } else { - $defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php"; + $defaultShareUrl = rtrim(BASE_URL, '/') . "api/file/share.php"; } define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl); diff --git a/copyFiles.php b/copyFiles.php deleted file mode 100644 index 6f98f62..0000000 --- a/copyFiles.php +++ /dev/null @@ -1,153 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to copy files."]); - exit(); - } -} - -$data = json_decode(file_get_contents("php://input"), true); -if ( - !$data || - !isset($data['source']) || - !isset($data['destination']) || - !isset($data['files']) -) { - echo json_encode(["error" => "Invalid request"]); - exit; -} - -$sourceFolder = trim($data['source']); -$destinationFolder = trim($data['destination']); -$files = $data['files']; - -// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes. -$folderPattern = REGEX_FOLDER_NAME; -if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) { - echo json_encode(["error" => "Invalid source folder name."]); - exit; -} -if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) { - echo json_encode(["error" => "Invalid destination folder name."]); - exit; -} - -// Trim any leading/trailing slashes and spaces. -$sourceFolder = trim($sourceFolder, "/\\ "); -$destinationFolder = trim($destinationFolder, "/\\ "); - -// Build the source and destination directories. -$baseDir = rtrim(UPLOAD_DIR, '/\\'); -$sourceDir = ($sourceFolder === 'root') - ? $baseDir . DIRECTORY_SEPARATOR - : $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; -$destDir = ($destinationFolder === 'root') - ? $baseDir . DIRECTORY_SEPARATOR - : $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; - -// Helper: Generate the metadata file path for a given folder. -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Helper: Generate a unique file name if a file with the same name exists. -function getUniqueFileName($destDir, $fileName) { - $fullPath = $destDir . $fileName; - clearstatcache(true, $fullPath); - if (!file_exists($fullPath)) { - return $fileName; - } - $basename = pathinfo($fileName, PATHINFO_FILENAME); - $extension = pathinfo($fileName, PATHINFO_EXTENSION); - $counter = 1; - do { - $newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : ""); - $newFullPath = $destDir . $newName; - clearstatcache(true, $newFullPath); - $counter++; - } while (file_exists($destDir . $newName)); - return $newName; -} - -// Load source and destination metadata. -$srcMetaFile = getMetadataFilePath($sourceFolder); -$destMetaFile = getMetadataFilePath($destinationFolder); - -$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : []; -$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; - -$errors = []; - -// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces. -$safeFileNamePattern = REGEX_FILE_NAME; - -foreach ($files as $fileName) { - // Save the original name for metadata lookup. - $originalName = basename(trim($fileName)); - $basename = $originalName; - if (!preg_match($safeFileNamePattern, $basename)) { - $errors[] = "$basename has an invalid name."; - continue; - } - - $srcPath = $sourceDir . $originalName; - $destPath = $destDir . $basename; - - clearstatcache(); - if (!file_exists($srcPath)) { - $errors[] = "$originalName does not exist in source."; - continue; - } - - if (file_exists($destPath)) { - $uniqueName = getUniqueFileName($destDir, $basename); - $basename = $uniqueName; // update the file name for metadata and destination path - $destPath = $destDir . $uniqueName; - } - - if (!copy($srcPath, $destPath)) { - $errors[] = "Failed to copy $basename"; - continue; - } - - // Update destination metadata: if there's metadata for the original file in source, add it under the new name. - if (isset($srcMetadata[$originalName])) { - $destMetadata[$basename] = $srcMetadata[$originalName]; - } -} - -if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { - $errors[] = "Failed to update destination metadata."; -} - -if (empty($errors)) { - echo json_encode(["success" => "Files copied successfully"]); -} else { - echo json_encode(["error" => implode("; ", $errors)]); -} -?> \ No newline at end of file diff --git a/createFolder.php b/createFolder.php deleted file mode 100644 index be2ded9..0000000 --- a/createFolder.php +++ /dev/null @@ -1,96 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -// Ensure the request is a POST -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - echo json_encode(['success' => false, 'error' => 'Invalid request method.']); - exit; -} - -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); - http_response_code(403); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to create folders."]); - exit(); - } -} - -// Get the JSON input and decode it -$input = json_decode(file_get_contents('php://input'), true); -if (!isset($input['folderName'])) { - echo json_encode(['success' => false, 'error' => 'Folder name not provided.']); - exit; -} - -$folderName = trim($input['folderName']); -$parent = isset($input['parent']) ? trim($input['parent']) : ""; - -// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName -if (!preg_match(REGEX_FOLDER_NAME, $folderName)) { - echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); - exit; -} - -// Optionally, sanitize the parent folder if needed. -if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) { - echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']); - exit; -} - -// Build the full folder path. -$baseDir = rtrim(UPLOAD_DIR, '/\\'); -if ($parent && strtolower($parent) !== "root") { - $fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName; - $relativePath = $parent . "/" . $folderName; -} else { - $fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName; - $relativePath = $folderName; -} - -// Check if the folder already exists. -if (file_exists($fullPath)) { - echo json_encode(['success' => false, 'error' => 'Folder already exists.']); - exit; -} - -// Attempt to create the folder. -if (mkdir($fullPath, 0755, true)) { - - // --- Create an empty metadata file for the new folder --- - // Helper: Generate the metadata file path for a given folder. - // For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". - function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; - } - - $metadataFile = getMetadataFilePath($relativePath); - // Create an empty associative array (i.e. empty metadata) and write to the metadata file. - file_put_contents($metadataFile, json_encode([], JSON_PRETTY_PRINT)); - - echo json_encode(['success' => true]); -} else { - echo json_encode(['success' => false, 'error' => 'Failed to create folder.']); -} -?> \ No newline at end of file diff --git a/createFolderShareLink.php b/createFolderShareLink.php deleted file mode 100644 index cc1da96..0000000 --- a/createFolderShareLink.php +++ /dev/null @@ -1,94 +0,0 @@ - "Invalid input."]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to create shared folders."]); - exit(); - } -} - -$folder = isset($input['folder']) ? trim($input['folder']) : ""; -$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60; -$password = isset($input['password']) ? $input['password'] : ""; -$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0; - -// Validate folder name using regex. -// Allow letters, numbers, underscores, hyphens, spaces and slashes. -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name."]); - exit; -} - -// Generate a secure token. -try { - $token = bin2hex(random_bytes(16)); // 32 hex characters. -} catch (Exception $e) { - echo json_encode(["error" => "Could not generate token."]); - exit; -} - -// Calculate expiration time (Unix timestamp). -$expires = time() + ($expirationMinutes * 60); - -// Hash password if provided. -$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : ""; - -// Define the file to store share folder links. -$shareFile = META_DIR . "share_folder_links.json"; -$shareLinks = []; -if (file_exists($shareFile)) { - $data = file_get_contents($shareFile); - $shareLinks = json_decode($data, true); - if (!is_array($shareLinks)) { - $shareLinks = []; - } -} - -// Clean up expired share links. -$currentTime = time(); -foreach ($shareLinks as $key => $link) { - if (isset($link["expires"]) && $link["expires"] < $currentTime) { - unset($shareLinks[$key]); - } -} - -// Add the new share record. -$shareLinks[$token] = [ - "folder" => $folder, - "expires" => $expires, - "password" => $hashedPassword, - "allowUpload" => $allowUpload -]; - -// Save the share links. -if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) { -// Determine base URL. -if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) { - $baseUrl = rtrim(BASE_URL, '/'); -} else { - // Prefer HTTP_HOST over SERVER_ADDR. - $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http"; - // Use HTTP_HOST if set; fallback to gethostbyname if needed. - $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost'); - $baseUrl = $protocol . "://" . $host; -} - // The share URL points to shareFolder.php. - $link = $baseUrl . "/shareFolder.php?token=" . urlencode($token); - echo json_encode(["token" => $token, "expires" => $expires, "link" => $link]); -} else { - echo json_encode(["error" => "Could not save share link."]); -} -?> \ No newline at end of file diff --git a/createShareLink.php b/createShareLink.php deleted file mode 100644 index 5e9d45b..0000000 --- a/createShareLink.php +++ /dev/null @@ -1,75 +0,0 @@ - "Invalid input."]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to create share files."]); - exit(); - } -} - -$folder = isset($input['folder']) ? trim($input['folder']) : ""; -$file = isset($input['file']) ? basename($input['file']) : ""; -$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60; -$password = isset($input['password']) ? $input['password'] : ""; - -// Validate folder using regex. -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name."]); - exit; -} - -// Generate a secure token. -$token = bin2hex(random_bytes(16)); // 32 hex characters. - -// Calculate expiration (Unix timestamp). -$expires = time() + ($expirationMinutes * 60); - -// Hash password if provided. -$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : ""; - -// File to store share links. -$shareFile = META_DIR . "share_links.json"; -$shareLinks = []; -if (file_exists($shareFile)) { - $data = file_get_contents($shareFile); - $shareLinks = json_decode($data, true); - if (!is_array($shareLinks)) { - $shareLinks = []; - } -} - -// Clean up expired share links. -$currentTime = time(); -foreach ($shareLinks as $key => $link) { - if ($link["expires"] < $currentTime) { - unset($shareLinks[$key]); - } -} - -// Add record. -$shareLinks[$token] = [ - "folder" => $folder, - "file" => $file, - "expires" => $expires, - "password" => $hashedPassword -]; - -// Save the share links. -if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) { - echo json_encode(["token" => $token, "expires" => $expires]); -} else { - echo json_encode(["error" => "Could not save share link."]); -} -?> \ No newline at end of file diff --git a/deleteFiles.php b/deleteFiles.php deleted file mode 100644 index 269e23f..0000000 --- a/deleteFiles.php +++ /dev/null @@ -1,161 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} - -// Define $username first. -$username = $_SESSION['username'] ?? ''; - -// Now load the user's permissions. -$userPermissions = loadUserPermissions($username); - -// Check if the user is read-only. -if ($username) { - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to delete files."]); - exit(); - } -} - -// --- Setup Trash Folder & Metadata --- -$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; -if (!file_exists($trashDir)) { - mkdir($trashDir, 0755, true); -} -$trashMetadataFile = $trashDir . "trash.json"; -$trashData = []; -if (file_exists($trashMetadataFile)) { - $json = file_get_contents($trashMetadataFile); - $trashData = json_decode($json, true); - if (!is_array($trashData)) { - $trashData = []; - } -} - -// Helper: Generate the metadata file path for a given folder. -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Read request body -$data = json_decode(file_get_contents("php://input"), true); - -// Validate request -if (!isset($data['files']) || !is_array($data['files'])) { - echo json_encode(["error" => "No file names provided"]); - exit; -} - -// Determine folder – default to 'root' -$folder = isset($data['folder']) ? trim($data['folder']) : 'root'; - -// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name."]); - exit; -} -$folder = trim($folder, "/\\ "); - -// Build the upload directory. -if ($folder !== 'root') { - $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; -} else { - $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; -} - -// Load folder metadata (if exists) to retrieve uploader and upload date. -$metadataFile = getMetadataFilePath($folder); -$folderMetadata = []; -if (file_exists($metadataFile)) { - $folderMetadata = json_decode(file_get_contents($metadataFile), true); - if (!is_array($folderMetadata)) { - $folderMetadata = []; - } -} - -$movedFiles = []; -$errors = []; - -// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces. -$safeFileNamePattern = REGEX_FILE_NAME; - -foreach ($data['files'] as $fileName) { - $basename = basename(trim($fileName)); - - // Validate the file name. - if (!preg_match($safeFileNamePattern, $basename)) { - $errors[] = "$basename has an invalid name."; - continue; - } - - $filePath = $uploadDir . $basename; - - if (file_exists($filePath)) { - // Append a timestamp to the file name in trash to avoid collisions. - $timestamp = time(); - $trashFileName = $basename . "_" . $timestamp; - if (rename($filePath, $trashDir . $trashFileName)) { - $movedFiles[] = $basename; - // Record trash metadata for possible restoration. - $trashData[] = [ - 'type' => 'file', - 'originalFolder' => $uploadDir, // You could also store a relative path here. - 'originalName' => $basename, - 'trashName' => $trashFileName, - 'trashedAt' => $timestamp, - // Enrich trash record with uploader and upload date from folder metadata (if available) - 'uploaded' => isset($folderMetadata[$basename]['uploaded']) ? $folderMetadata[$basename]['uploaded'] : "Unknown", - 'uploader' => isset($folderMetadata[$basename]['uploader']) ? $folderMetadata[$basename]['uploader'] : "Unknown", - // NEW: Record the username of the user who deleted the file. - 'deletedBy' => isset($_SESSION['username']) ? $_SESSION['username'] : "Unknown" - ]; - } else { - $errors[] = "Failed to move $basename to Trash."; - } - } else { - // Consider file already deleted. - $movedFiles[] = $basename; - } -} - -// Write back the updated trash metadata. -file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT)); - -// Update folder-specific metadata file by removing deleted files. -if (file_exists($metadataFile)) { - $metadata = json_decode(file_get_contents($metadataFile), true); - if (is_array($metadata)) { - foreach ($movedFiles as $delFile) { - if (isset($metadata[$delFile])) { - unset($metadata[$delFile]); - } - } - file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); - } -} - -if (empty($errors)) { - echo json_encode(["success" => "Files moved to Trash: " . implode(", ", $movedFiles)]); -} else { - echo json_encode(["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)]); -} -?> \ No newline at end of file diff --git a/deleteFolder.php b/deleteFolder.php deleted file mode 100644 index a97371d..0000000 --- a/deleteFolder.php +++ /dev/null @@ -1,99 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -// Ensure the request is a POST -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - echo json_encode(['success' => false, 'error' => 'Invalid request method.']); - exit; -} - -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); - http_response_code(403); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to delete folders."]); - exit(); - } -} - -// Get the JSON input and decode it -$input = json_decode(file_get_contents('php://input'), true); -if (!isset($input['folder'])) { - echo json_encode(['success' => false, 'error' => 'Folder name not provided.']); - exit; -} - -$folderName = trim($input['folder']); - -// Prevent deletion of root. -if ($folderName === 'root') { - echo json_encode(['success' => false, 'error' => 'Cannot delete root folder.']); - exit; -} - -// Allow letters, numbers, underscores, dashes, spaces, and forward slashes. -if (!preg_match(REGEX_FOLDER_NAME, $folderName)) { - echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); - exit; -} - -// Build the folder path (supports subfolder paths like "FolderTest/FolderTestSub") -$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName; - -// Check if the folder exists and is a directory -if (!file_exists($folderPath) || !is_dir($folderPath)) { - echo json_encode(['success' => false, 'error' => 'Folder does not exist.']); - exit; -} - -// Prevent deletion if the folder is not empty -if (count(scandir($folderPath)) > 2) { - echo json_encode(['success' => false, 'error' => 'Folder is not empty.']); - exit; -} - -/** - * Helper: Generate the metadata file path for a given folder. - * For "root", returns "root_metadata.json". Otherwise, it replaces - * slashes, backslashes, and spaces with dashes and appends "_metadata.json". - * - * @param string $folder The folder's relative path. - * @return string The full path to the folder's metadata file. - */ -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Attempt to delete the folder. -if (rmdir($folderPath)) { - // Remove corresponding metadata file if it exists. - $metadataFile = getMetadataFilePath($folderName); - if (file_exists($metadataFile)) { - unlink($metadataFile); - } - echo json_encode(['success' => true]); -} else { - echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']); -} -?> \ No newline at end of file diff --git a/deleteTrashFiles.php b/deleteTrashFiles.php deleted file mode 100644 index 666e911..0000000 --- a/deleteTrashFiles.php +++ /dev/null @@ -1,104 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} - -// --- Setup Trash Folder & Metadata --- -$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; -if (!file_exists($trashDir)) { - mkdir($trashDir, 0755, true); -} -$trashMetadataFile = $trashDir . "trash.json"; - -// Load trash metadata into an associative array keyed by trashName. -$trashData = []; -if (file_exists($trashMetadataFile)) { - $json = file_get_contents($trashMetadataFile); - $tempData = json_decode($json, true); - if (is_array($tempData)) { - foreach ($tempData as $item) { - if (isset($item['trashName'])) { - $trashData[$item['trashName']] = $item; - } - } - } -} - -// Read request body. -$data = json_decode(file_get_contents("php://input"), true); -if (!$data) { - echo json_encode(["error" => "Invalid input"]); - exit; -} - -// Determine deletion mode: if "deleteAll" is true, delete all trash items; otherwise, use provided "files" array. -$filesToDelete = []; -if (isset($data['deleteAll']) && $data['deleteAll'] === true) { - $filesToDelete = array_keys($trashData); -} elseif (isset($data['files']) && is_array($data['files'])) { - $filesToDelete = $data['files']; -} else { - echo json_encode(["error" => "No trash file identifiers provided"]); - exit; -} - -$deletedFiles = []; -$errors = []; - -// Define a safe file name pattern. -$safeFileNamePattern = REGEX_FILE_NAME; - -foreach ($filesToDelete as $trashName) { - $trashName = trim($trashName); - if (!preg_match($safeFileNamePattern, $trashName)) { - $errors[] = "$trashName has an invalid format."; - continue; - } - - if (!isset($trashData[$trashName])) { - $errors[] = "Trash item $trashName not found."; - continue; - } - - $filePath = $trashDir . $trashName; - - if (file_exists($filePath)) { - if (unlink($filePath)) { - $deletedFiles[] = $trashName; - unset($trashData[$trashName]); - } else { - $errors[] = "Failed to delete $trashName."; - } - } else { - // If the file doesn't exist, remove its metadata entry. - unset($trashData[$trashName]); - $deletedFiles[] = $trashName; - } -} - -// Write the updated trash metadata back (as an indexed array). -file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); - -if (empty($errors)) { - echo json_encode(["success" => "Trash items deleted: " . implode(", ", $deletedFiles)]); -} else { - echo json_encode(["error" => implode("; ", $errors) . ". Trash items deleted: " . implode(", ", $deletedFiles)]); -} -exit; -?> \ No newline at end of file diff --git a/download.php b/download.php deleted file mode 100644 index 6b011a5..0000000 --- a/download.php +++ /dev/null @@ -1,85 +0,0 @@ - "Unauthorized"]); - exit; -} - -// Get file parameters from the GET request. -$file = isset($_GET['file']) ? basename($_GET['file']) : ''; -$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; - -// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses) -if (!preg_match(REGEX_FILE_NAME, $file)) { - http_response_code(400); - echo json_encode(["error" => "Invalid file name."]); - exit; -} - -// Get the realpath of the upload directory. -$uploadDirReal = realpath(UPLOAD_DIR); -if ($uploadDirReal === false) { - http_response_code(500); - echo json_encode(["error" => "Server misconfiguration."]); - exit; -} - -// Determine the directory. -if ($folder === 'root') { - $directory = $uploadDirReal; -} else { - // Prevent path traversal in folder parameter. - if (strpos($folder, '..') !== false) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder name."]); - exit; - } - - $directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; - $directory = realpath($directoryPath); - - // Ensure that the resolved directory exists and is within the allowed UPLOAD_DIR. - if ($directory === false || strpos($directory, $uploadDirReal) !== 0) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder path."]); - exit; - } -} - -// Build the file path. -$filePath = $directory . DIRECTORY_SEPARATOR . $file; -$realFilePath = realpath($filePath); - -// Validate that the real file path exists and is within the allowed directory. -if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) { - http_response_code(403); - echo json_encode(["error" => "Access forbidden."]); - exit; -} - -if (!file_exists($realFilePath)) { - http_response_code(404); - echo json_encode(["error" => "File not found."]); - exit; -} - -// Serve the file. -$mimeType = mime_content_type($realFilePath); -header("Content-Type: " . $mimeType); - -// For images, serve inline; for other types, force download. -$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); -if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) { - header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); -} else { - header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); -} -header('Content-Length: ' . filesize($realFilePath)); - -readfile($realFilePath); -exit; -?> \ No newline at end of file diff --git a/downloadSharedFile.php b/downloadSharedFile.php deleted file mode 100644 index 8a77dc5..0000000 --- a/downloadSharedFile.php +++ /dev/null @@ -1,85 +0,0 @@ - $record['expires']) { - http_response_code(403); - echo "This share link has expired."; - exit; -} - -// Get the shared folder from the record. -$folder = trim($record['folder'], "/\\ "); -$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; -$realFolderPath = realpath($folderPath); -$uploadDirReal = realpath(UPLOAD_DIR); - -if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { - http_response_code(404); - echo "Shared folder not found."; - exit; -} - -// Sanitize the filename to prevent directory traversal. -if (strpos($file, "/") !== false || strpos($file, "\\") !== false) { - http_response_code(400); - echo "Invalid file name."; - exit; -} -$file = basename($file); - -// Build the full file path and verify it is inside the shared folder. -$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file; -$realFilePath = realpath($filePath); -if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) { - http_response_code(404); - echo "File not found."; - exit; -} - -// Determine MIME type. -$mimeType = mime_content_type($realFilePath); -header("Content-Type: " . $mimeType); - -// Set Content-Disposition header. -// Inline if the file is an image; attachment for others. -$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); -if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) { - header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); -} else { - header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); -} - -// Read and output the file. -readfile($realFilePath); -exit; -?> \ No newline at end of file diff --git a/downloadZip.php b/downloadZip.php deleted file mode 100644 index 4b7ec44..0000000 --- a/downloadZip.php +++ /dev/null @@ -1,133 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Check if the user is authenticated. -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - http_response_code(401); - header('Content-Type: application/json'); - echo json_encode(["error" => "Unauthorized"]); - exit; -} - -// Read and decode the JSON input. -$rawData = file_get_contents("php://input"); -$data = json_decode($rawData, true); - -if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) { - http_response_code(400); - header('Content-Type: application/json'); - echo json_encode(["error" => "Invalid input."]); - exit; -} - -$folder = $data['folder']; -$files = $data['files']; - -// Validate folder name to allow subfolders. -// "root" is allowed; otherwise, split by "/" and validate each segment. -if ($folder !== "root") { - $parts = explode('/', $folder); - foreach ($parts as $part) { - if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) { - http_response_code(400); - header('Content-Type: application/json'); - echo json_encode(["error" => "Invalid folder name."]); - exit; - } - } - $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR; -} else { - $relativePath = ""; -} - -// Use the absolute UPLOAD_DIR from config.php. -$baseDir = realpath(UPLOAD_DIR); -if ($baseDir === false) { - http_response_code(500); - header('Content-Type: application/json'); - echo json_encode(["error" => "Uploads directory not configured correctly."]); - exit; -} - -$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath; -$folderPathReal = realpath($folderPath); -if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { - http_response_code(404); - header('Content-Type: application/json'); - echo json_encode(["error" => "Folder not found."]); - exit; -} - -if (empty($files)) { - http_response_code(400); - header('Content-Type: application/json'); - echo json_encode(["error" => "No files specified."]); - exit; -} - -foreach ($files as $fileName) { - if (!preg_match(REGEX_FILE_NAME, $fileName)) { - http_response_code(400); - header('Content-Type: application/json'); - echo json_encode(["error" => "Invalid file name: " . $fileName]); - exit; - } -} - -// Build an array of files to include in the ZIP. -$filesToZip = []; -foreach ($files as $fileName) { - $filePath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName; - if (file_exists($filePath)) { - $filesToZip[] = $filePath; - } -} - -if (empty($filesToZip)) { - http_response_code(400); - header('Content-Type: application/json'); - echo json_encode(["error" => "No valid files found to zip."]); - exit; -} - -// Create a temporary file for the ZIP archive. -$tempZip = tempnam(sys_get_temp_dir(), 'zip'); -unlink($tempZip); // Remove the temporary file so ZipArchive can create a new one. -$tempZip .= '.zip'; - -$zip = new ZipArchive(); -if ($zip->open($tempZip, ZipArchive::CREATE) !== TRUE) { - http_response_code(500); - header('Content-Type: application/json'); - echo json_encode(["error" => "Could not create zip archive."]); - exit; -} - -// Add each file to the archive using its base name. -foreach ($filesToZip as $filePath) { - $zip->addFile($filePath, basename($filePath)); -} -$zip->close(); - -// Send headers to force download and disable caching. -header('Content-Type: application/zip'); -header('Content-Disposition: attachment; filename="files.zip"'); -header('Content-Length: ' . filesize($tempZip)); -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Pragma: no-cache'); - -// Output the file and delete it afterward. -readfile($tempZip); -unlink($tempZip); -exit; -?> \ No newline at end of file diff --git a/extractZip.php b/extractZip.php deleted file mode 100644 index 5bad5e6..0000000 --- a/extractZip.php +++ /dev/null @@ -1,165 +0,0 @@ - "Invalid CSRF token"]); - exit; -} - -// Ensure user is authenticated. -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - http_response_code(401); - echo json_encode(["error" => "Unauthorized"]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to extract zip files"]); - exit(); - } -} - -// Read and decode the JSON input. -$rawData = file_get_contents("php://input"); -$data = json_decode($rawData, true); -if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) { - http_response_code(400); - echo json_encode(["error" => "Invalid input."]); - exit; -} - -$folder = $data['folder']; -$files = $data['files']; - -if (empty($files)) { - http_response_code(400); - echo json_encode(["error" => "No files specified."]); - exit; -} - -// Validate folder name (allow "root" or valid subfolder names). -if ($folder !== "root") { - $parts = explode('/', $folder); - foreach ($parts as $part) { - if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder name."]); - exit; - } - } - $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR; -} else { - $relativePath = ""; -} - -$baseDir = realpath(UPLOAD_DIR); -if ($baseDir === false) { - http_response_code(500); - echo json_encode(["error" => "Uploads directory not configured correctly."]); - exit; -} - -$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath; -$folderPathReal = realpath($folderPath); -if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { - http_response_code(404); - echo json_encode(["error" => "Folder not found."]); - exit; -} - -// ---------- Metadata Setup ---------- -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -$srcMetaFile = getMetadataFilePath($folder); -$destMetaFile = getMetadataFilePath($folder); -$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : []; -$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; - -$errors = []; -$allSuccess = true; -$extractedFiles = array(); // Array to collect names of extracted files -$safeFileNamePattern = REGEX_FILE_NAME; - -// ---------- Process Each File ---------- -foreach ($files as $zipFileName) { - $originalName = basename(trim($zipFileName)); - // Process only .zip files. - if (strtolower(substr($originalName, -4)) !== '.zip') { - continue; - } - if (!preg_match($safeFileNamePattern, $originalName)) { - $errors[] = "$originalName has an invalid name."; - $allSuccess = false; - continue; - } - - $zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName; - if (!file_exists($zipFilePath)) { - $errors[] = "$originalName does not exist in folder."; - $allSuccess = false; - continue; - } - - $zip = new ZipArchive(); - if ($zip->open($zipFilePath) !== TRUE) { - $errors[] = "Could not open $originalName as a zip file."; - $allSuccess = false; - continue; - } - - // Attempt extraction. - if (!$zip->extractTo($folderPathReal)) { - $errors[] = "Failed to extract $originalName."; - $allSuccess = false; - } else { - // Collect extracted file names from this zip. - for ($i = 0; $i < $zip->numFiles; $i++) { - $entryName = $zip->getNameIndex($i); - $extractedFileName = basename($entryName); - if ($extractedFileName) { - $extractedFiles[] = $extractedFileName; - } - } - // Update metadata for each extracted file if the zip file has metadata. - if (isset($srcMetadata[$originalName])) { - $zipMeta = $srcMetadata[$originalName]; - // Iterate through all entries in the zip. - for ($i = 0; $i < $zip->numFiles; $i++) { - $entryName = $zip->getNameIndex($i); - $extractedFileName = basename($entryName); - if ($extractedFileName) { - $destMetadata[$extractedFileName] = $zipMeta; - } - } - } - } - $zip->close(); -} - -// Write updated metadata back to the destination metadata file. -if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { - $errors[] = "Failed to update metadata."; - $allSuccess = false; -} - -if ($allSuccess) { - echo json_encode(["success" => true, "extractedFiles" => $extractedFiles]); -} else { - echo json_encode(["success" => false, "error" => implode(" ", $errors)]); -} -exit; -?> \ No newline at end of file diff --git a/getConfig.php b/getConfig.php deleted file mode 100644 index a9ca029..0000000 --- a/getConfig.php +++ /dev/null @@ -1,46 +0,0 @@ - 'Failed to decrypt configuration.']); - exit; - } - // Decode the configuration and ensure required fields are set - $config = json_decode($decryptedContent, true); - - // Ensure globalOtpauthUrl is set - if (!isset($config['globalOtpauthUrl'])) { - $config['globalOtpauthUrl'] = ""; - } - - // NEW: Ensure header_title is set. - if (!isset($config['header_title']) || empty($config['header_title'])) { - $config['header_title'] = "FileRise"; // default value - } - - echo json_encode($config); -} else { - // If no config file exists, provide defaults - echo json_encode([ - 'header_title' => "FileRise", - 'oidc' => [ - 'providerUrl' => 'https://your-oidc-provider.com', - 'clientId' => 'YOUR_CLIENT_ID', - 'clientSecret' => 'YOUR_CLIENT_SECRET', - 'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback' - ], - 'loginOptions' => [ - 'disableFormLogin' => false, - 'disableBasicAuth' => false, - 'disableOIDCLogin' => false - ], - 'globalOtpauthUrl' => "" - ]); -} -?> \ No newline at end of file diff --git a/getFileList.php b/getFileList.php deleted file mode 100644 index ee446b0..0000000 --- a/getFileList.php +++ /dev/null @@ -1,106 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; -// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes). -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name."]); - exit; -} - -// Determine the directory based on the folder parameter. -if ($folder !== 'root') { - $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; -} else { - $directory = UPLOAD_DIR; -} - -/** - * Helper: Generate the metadata file path for a given folder. - */ -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -$metadataFile = getMetadataFilePath($folder); -$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; - -if (!is_dir($directory)) { - echo json_encode(["error" => "Directory not found."]); - exit; -} - -$files = array_values(array_diff(scandir($directory), array('.', '..'))); -$fileList = []; - -// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces. -$safeFileNamePattern = REGEX_FILE_NAME; - -foreach ($files as $file) { - // Skip hidden files (those that begin with a dot) - if (substr($file, 0, 1) === '.') { - continue; - } - - $filePath = $directory . DIRECTORY_SEPARATOR . $file; - // Only include files (skip directories) - if (!is_file($filePath)) continue; - - // Optionally, skip files with unsafe names. - if (!preg_match($safeFileNamePattern, $file)) { - continue; - } - - // Since metadata is stored per folder, the key is simply the file name. - $metaKey = $file; - $fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown"; - $fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown"; - $fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown"; - - $fileSizeBytes = filesize($filePath); - if ($fileSizeBytes >= 1073741824) { - $fileSizeFormatted = sprintf("%.1f GB", $fileSizeBytes / 1073741824); - } elseif ($fileSizeBytes >= 1048576) { - $fileSizeFormatted = sprintf("%.1f MB", $fileSizeBytes / 1048576); - } elseif ($fileSizeBytes >= 1024) { - $fileSizeFormatted = sprintf("%.1f KB", $fileSizeBytes / 1024); - } else { - $fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes)); - } - - // Build the basic file entry. - $fileEntry = [ - 'name' => $file, - 'modified' => $fileDateModified, - 'uploaded' => $fileUploadedDate, - 'size' => $fileSizeFormatted, - 'uploader' => $fileUploader, - 'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : [] - ]; - - // Add file content for text-based files. - if (preg_match('/\.(txt|html|htm|md|js|css|json|xml|php|py|ini|conf|log)$/i', $file)) { - $content = file_get_contents($filePath); - $fileEntry['content'] = $content; - } - - $fileList[] = $fileEntry; -} - -// Load global tags from createdTags.json. -$globalTagsFile = META_DIR . "createdTags.json"; -$globalTags = file_exists($globalTagsFile) ? json_decode(file_get_contents($globalTagsFile), true) : []; - -echo json_encode(["files" => $fileList, "globalTags" => $globalTags]); -?> \ No newline at end of file diff --git a/getFileTag.php b/getFileTag.php deleted file mode 100644 index 9fd63ed..0000000 --- a/getFileTag.php +++ /dev/null @@ -1,40 +0,0 @@ - "Unable to read metadata file."]); - exit; -} - -// Decode the JSON data to check for validity -$jsonData = json_decode($data, true); -if (json_last_error() !== JSON_ERROR_NONE) { - error_log('Invalid JSON in metadata file: ' . $metadataPath . ' Error: ' . json_last_error_msg()); - http_response_code(500); - echo json_encode(["error" => "Metadata file contains invalid JSON."]); - exit; -} - -// Output the re-encoded JSON to ensure well-formed output -echo json_encode($jsonData); -exit; \ No newline at end of file diff --git a/getFolderList.php b/getFolderList.php deleted file mode 100644 index cf61b22..0000000 --- a/getFolderList.php +++ /dev/null @@ -1,97 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -/** - * Recursively scan a directory for subfolders. - * - * @param string $dir The full path to the directory. - * @param string $relative The relative path from the base upload directory. - * @return array An array of folder paths (relative to the base). - */ -function getSubfolders($dir, $relative = '') { - $folders = []; - $items = scandir($dir); - // Allow letters, numbers, underscores, dashes, and spaces in folder names. - $safeFolderNamePattern = REGEX_FOLDER_NAME; - foreach ($items as $item) { - if ($item === '.' || $item === '..') continue; - if (!preg_match($safeFolderNamePattern, $item)) { - continue; - } - $path = $dir . DIRECTORY_SEPARATOR . $item; - if (is_dir($path)) { - // Build the relative path. - $folderPath = ($relative ? $relative . '/' : '') . $item; - $folders[] = $folderPath; - // Recursively get subfolders. - $subFolders = getSubfolders($path, $folderPath); - $folders = array_merge($folders, $subFolders); - } - } - return $folders; -} - -/** - * Helper: Generate the metadata file path for a given folder. - * For "root", it returns "root_metadata.json"; otherwise, it replaces - * slashes, backslashes, and spaces with dashes and appends "_metadata.json". - * - * @param string $folder The folder's relative path. - * @return string The full path to the folder's metadata file. - */ -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -$baseDir = rtrim(UPLOAD_DIR, '/\\'); - -// Build an array to hold folder information. -$folderInfoList = []; - -// Include "root" as a folder. -$rootMetaFile = getMetadataFilePath('root'); -$rootFileCount = 0; -if (file_exists($rootMetaFile)) { - $rootMetadata = json_decode(file_get_contents($rootMetaFile), true); - $rootFileCount = is_array($rootMetadata) ? count($rootMetadata) : 0; -} -$folderInfoList[] = [ - "folder" => "root", - "fileCount" => $rootFileCount, - "metadataFile" => basename($rootMetaFile) -]; - -// Scan for subfolders. -$subfolders = []; -if (is_dir($baseDir)) { - $subfolders = getSubfolders($baseDir); -} - -// For each subfolder, load its metadata and record file count. -foreach ($subfolders as $folder) { - $metaFile = getMetadataFilePath($folder); - $fileCount = 0; - if (file_exists($metaFile)) { - $metadata = json_decode(file_get_contents($metaFile), true); - $fileCount = is_array($metadata) ? count($metadata) : 0; - } - $folderInfoList[] = [ - "folder" => $folder, - "fileCount" => $fileCount, - "metadataFile" => basename($metaFile) - ]; -} - -echo json_encode($folderInfoList); -?> \ No newline at end of file diff --git a/getTrashItems.php b/getTrashItems.php deleted file mode 100644 index f3ef7f0..0000000 --- a/getTrashItems.php +++ /dev/null @@ -1,68 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -// Define the trash directory and trash metadata file. -$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; -$trashMetadataFile = $trashDir . "trash.json"; - -// Helper: Generate the metadata file path for a given folder. -// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Read the trash metadata. -$trashItems = []; -if (file_exists($trashMetadataFile)) { - $json = file_get_contents($trashMetadataFile); - $trashItems = json_decode($json, true); - if (!is_array($trashItems)) { - $trashItems = []; - } -} - -// Enrich each trash record. -foreach ($trashItems as &$item) { - // Ensure deletedBy is set and not empty. - if (empty($item['deletedBy'])) { - $item['deletedBy'] = "Unknown"; - } - // Enrich with uploader and uploaded date if not already present. - if (empty($item['uploaded']) || empty($item['uploader'])) { - if (isset($item['originalFolder']) && isset($item['originalName'])) { - $metadataFile = getMetadataFilePath($item['originalFolder']); - if (file_exists($metadataFile)) { - $metadata = json_decode(file_get_contents($metadataFile), true); - if (is_array($metadata) && isset($metadata[$item['originalName']])) { - $item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown"; - $item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown"; - } else { - $item['uploaded'] = "Unknown"; - $item['uploader'] = "Unknown"; - } - } else { - $item['uploaded'] = "Unknown"; - $item['uploader'] = "Unknown"; - } - } else { - $item['uploaded'] = "Unknown"; - $item['uploader'] = "Unknown"; - } - } -} -unset($item); - -echo json_encode($trashItems); -exit; -?> \ No newline at end of file diff --git a/getUserPermissions.php b/getUserPermissions.php deleted file mode 100644 index 4d46845..0000000 --- a/getUserPermissions.php +++ /dev/null @@ -1,47 +0,0 @@ - "Unauthorized"]); - exit; -} - -$permissionsFile = USERS_DIR . "userPermissions.json"; -$permissionsArray = []; - -// Load permissions file if it exists. -if (file_exists($permissionsFile)) { - $content = file_get_contents($permissionsFile); - // Attempt to decrypt the content. - $decryptedContent = decryptData($content, $encryptionKey); - if ($decryptedContent === false) { - // If decryption fails, assume the file is plain JSON. - $permissionsArray = json_decode($content, true); - } else { - $permissionsArray = json_decode($decryptedContent, true); - } - if (!is_array($permissionsArray)) { - $permissionsArray = []; - } -} - -// If the user is an admin, return all permissions. -if (isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true) { - echo json_encode($permissionsArray); - exit; -} - -// Otherwise, return only the current user's permissions. -$username = $_SESSION['username'] ?? ''; -foreach ($permissionsArray as $storedUsername => $data) { - if (strcasecmp($storedUsername, $username) === 0) { - echo json_encode($data); - exit; - } -} - -// If no permissions are found for the current user, return an empty object. -echo json_encode(new stdClass()); -?> \ No newline at end of file diff --git a/getUsers.php b/getUsers.php deleted file mode 100644 index 6d1efdc..0000000 --- a/getUsers.php +++ /dev/null @@ -1,31 +0,0 @@ - "Unauthorized"]); - exit; -} - -$usersFile = USERS_DIR . USERS_FILE; -$users = []; - -if (file_exists($usersFile)) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) >= 3) { - // Validate username format: - if (preg_match(REGEX_USER, $parts[0])) { - $users[] = [ - "username" => $parts[0], - "role" => trim($parts[2]) - ]; - } - } - } -} - -echo json_encode($users); -?> \ No newline at end of file diff --git a/login_basic.php b/login_basic.php deleted file mode 100644 index ded910e..0000000 --- a/login_basic.php +++ /dev/null @@ -1,120 +0,0 @@ -= 4 && $parts[0] === $username && !empty($parts[3])) { - return decryptData($parts[3], $encryptionKey); - } - } - return null; -} - -// Reuse the same authentication function -function authenticate($username, $password) -{ - global $usersFile; - if (!file_exists($usersFile)) { - error_log("authenticate(): users file not found"); - return false; - } - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3); - if ($username === $storedUser && password_verify($password, $storedPass)) { - return $storedRole; // Return the user's role - } - } - error_log("authenticate(): authentication failed for '$username'"); - return false; -} - -// Define helper function to get a user's role from users.txt -function getUserRole($username) { - global $usersFile; - if (file_exists($usersFile)) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(":", trim($line)); - if (count($parts) >= 3 && $parts[0] === $username) { - return trim($parts[2]); - } - } - } - return null; -} - -// Add the loadFolderPermission function here: -function loadFolderPermission($username) { - global $encryptionKey; - $permissionsFile = USERS_DIR . 'userPermissions.json'; - if (file_exists($permissionsFile)) { - $content = file_get_contents($permissionsFile); - $decrypted = decryptData($content, $encryptionKey); - $permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true); - if (is_array($permissions)) { - foreach ($permissions as $storedUsername => $data) { - if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) { - return (bool)$data['folderOnly']; - } - } - } - } - return false; -} - -// Check if the user has sent HTTP Basic auth credentials. -if (!isset($_SERVER['PHP_AUTH_USER'])) { - header('WWW-Authenticate: Basic realm="FileRise Login"'); - header('HTTP/1.0 401 Unauthorized'); - echo 'Authorization Required'; - exit; -} - -$username = trim($_SERVER['PHP_AUTH_USER']); -$password = trim($_SERVER['PHP_AUTH_PW']); - -// Validate username format (optional) -if (!preg_match(REGEX_USER, $username)) { - header('WWW-Authenticate: Basic realm="FileRise Login"'); - header('HTTP/1.0 401 Unauthorized'); - echo 'Invalid username format'; - exit; -} - -// Attempt authentication -$roleFromAuth = authenticate($username, $password); -if ($roleFromAuth !== false) { - // --- NEW: check for TOTP secret --- - $secret = getUserTOTPSecret($username); - if ($secret) { - // hold user & secret in session and ask client for TOTP - $_SESSION['pending_login_user'] = $username; - $_SESSION['pending_login_secret'] = $secret; - header("Location: index.html?totp_required=1"); - exit; - } - - // no TOTP, proceed as before - session_regenerate_id(true); - $_SESSION["authenticated"] = true; - $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = (getUserRole($username) === "1"); - $_SESSION["folderOnly"] = loadFolderPermission($username); - - header("Location: index.html"); - exit; -} - -// Invalid credentials; prompt again -header('WWW-Authenticate: Basic realm="FileRise Login"'); -header('HTTP/1.0 401 Unauthorized'); -echo 'Invalid credentials'; -exit; -?> \ No newline at end of file diff --git a/logout.php b/logout.php deleted file mode 100644 index a8c2d3e..0000000 --- a/logout.php +++ /dev/null @@ -1,50 +0,0 @@ - \ No newline at end of file diff --git a/moveFiles.php b/moveFiles.php deleted file mode 100644 index 9874740..0000000 --- a/moveFiles.php +++ /dev/null @@ -1,164 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to move files."]); - exit(); - } -} - -$data = json_decode(file_get_contents("php://input"), true); -if ( - !$data || - !isset($data['source']) || - !isset($data['destination']) || - !isset($data['files']) -) { - echo json_encode(["error" => "Invalid request"]); - exit; -} - -$sourceFolder = trim($data['source']) ?: 'root'; -$destinationFolder = trim($data['destination']) ?: 'root'; - -// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names. -$folderPattern = REGEX_FOLDER_NAME; -if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) { - echo json_encode(["error" => "Invalid source folder name."]); - exit; -} -if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) { - echo json_encode(["error" => "Invalid destination folder name."]); - exit; -} - -// Remove any leading/trailing slashes. -$sourceFolder = trim($sourceFolder, "/\\ "); -$destinationFolder = trim($destinationFolder, "/\\ "); - -// Build the source and destination directories. -$baseDir = rtrim(UPLOAD_DIR, '/\\'); -$sourceDir = ($sourceFolder === 'root') - ? $baseDir . DIRECTORY_SEPARATOR - : $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; -$destDir = ($destinationFolder === 'root') - ? $baseDir . DIRECTORY_SEPARATOR - : $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; - -// Ensure destination directory exists. -if (!is_dir($destDir)) { - if (!mkdir($destDir, 0775, true)) { - echo json_encode(["error" => "Could not create destination folder"]); - exit; - } -} - -// Helper: Generate the metadata file path for a given folder. -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Helper: Generate a unique file name if a file with the same name exists. -function getUniqueFileName($destDir, $fileName) { - $fullPath = $destDir . $fileName; - clearstatcache(true, $fullPath); - if (!file_exists($fullPath)) { - return $fileName; - } - $basename = pathinfo($fileName, PATHINFO_FILENAME); - $extension = pathinfo($fileName, PATHINFO_EXTENSION); - $counter = 1; - do { - $newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : ""); - $newFullPath = $destDir . $newName; - clearstatcache(true, $newFullPath); - $counter++; - } while (file_exists($destDir . $newName)); - return $newName; -} - -// Prepare metadata files. -$srcMetaFile = getMetadataFilePath($sourceFolder); -$destMetaFile = getMetadataFilePath($destinationFolder); - -$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : []; -$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; - -$errors = []; -$safeFileNamePattern = REGEX_FILE_NAME; - -foreach ($data['files'] as $fileName) { - // Save the original name for metadata lookup. - $originalName = basename(trim($fileName)); - $basename = $originalName; // Start with the original name. - - // Validate the file name. - if (!preg_match($safeFileNamePattern, $basename)) { - $errors[] = "$basename has invalid characters."; - continue; - } - - $srcPath = $sourceDir . $originalName; - $destPath = $destDir . $basename; - - clearstatcache(); - if (!file_exists($srcPath)) { - $errors[] = "$originalName does not exist in source."; - continue; - } - - // If a file with the same name exists in destination, generate a unique name. - if (file_exists($destPath)) { - $uniqueName = getUniqueFileName($destDir, $basename); - $basename = $uniqueName; - $destPath = $destDir . $uniqueName; - } - - if (!rename($srcPath, $destPath)) { - $errors[] = "Failed to move $basename"; - continue; - } - - // Update metadata: if there is metadata for the original file, move it under the new name. - if (isset($srcMetadata[$originalName])) { - $destMetadata[$basename] = $srcMetadata[$originalName]; - unset($srcMetadata[$originalName]); - } -} - -if (file_put_contents($srcMetaFile, json_encode($srcMetadata, JSON_PRETTY_PRINT)) === false) { - $errors[] = "Failed to update source metadata."; -} -if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { - $errors[] = "Failed to update destination metadata."; -} - -if (empty($errors)) { - echo json_encode(["success" => "Files moved successfully"]); -} else { - echo json_encode(["error" => implode("; ", $errors)]); -} -?> \ No newline at end of file diff --git a/public/api/addUser.php b/public/api/addUser.php new file mode 100644 index 0000000..7bed442 --- /dev/null +++ b/public/api/addUser.php @@ -0,0 +1,8 @@ +addUser(); \ No newline at end of file diff --git a/public/api/admin/getConfig.php b/public/api/admin/getConfig.php new file mode 100644 index 0000000..fba6d4a --- /dev/null +++ b/public/api/admin/getConfig.php @@ -0,0 +1,8 @@ +getConfig(); \ No newline at end of file diff --git a/public/api/admin/updateConfig.php b/public/api/admin/updateConfig.php new file mode 100644 index 0000000..9aeb185 --- /dev/null +++ b/public/api/admin/updateConfig.php @@ -0,0 +1,8 @@ +updateConfig(); \ No newline at end of file diff --git a/public/api/auth/auth.php b/public/api/auth/auth.php new file mode 100644 index 0000000..a31d8f9 --- /dev/null +++ b/public/api/auth/auth.php @@ -0,0 +1,9 @@ +auth(); \ No newline at end of file diff --git a/public/api/auth/checkAuth.php b/public/api/auth/checkAuth.php new file mode 100644 index 0000000..f274438 --- /dev/null +++ b/public/api/auth/checkAuth.php @@ -0,0 +1,8 @@ +checkAuth(); \ No newline at end of file diff --git a/public/api/auth/login_basic.php b/public/api/auth/login_basic.php new file mode 100644 index 0000000..cd81ef8 --- /dev/null +++ b/public/api/auth/login_basic.php @@ -0,0 +1,8 @@ +loginBasic(); \ No newline at end of file diff --git a/public/api/auth/logout.php b/public/api/auth/logout.php new file mode 100644 index 0000000..3fe9012 --- /dev/null +++ b/public/api/auth/logout.php @@ -0,0 +1,8 @@ +logout(); \ No newline at end of file diff --git a/public/api/auth/token.php b/public/api/auth/token.php new file mode 100644 index 0000000..e6dbc59 --- /dev/null +++ b/public/api/auth/token.php @@ -0,0 +1,8 @@ +getToken(); \ No newline at end of file diff --git a/public/api/changePassword.php b/public/api/changePassword.php new file mode 100644 index 0000000..468287f --- /dev/null +++ b/public/api/changePassword.php @@ -0,0 +1,8 @@ +changePassword(); \ No newline at end of file diff --git a/public/api/file/copyFiles.php b/public/api/file/copyFiles.php new file mode 100644 index 0000000..2afed0d --- /dev/null +++ b/public/api/file/copyFiles.php @@ -0,0 +1,8 @@ +copyFiles(); \ No newline at end of file diff --git a/public/api/file/createShareLink.php b/public/api/file/createShareLink.php new file mode 100644 index 0000000..6d5ac35 --- /dev/null +++ b/public/api/file/createShareLink.php @@ -0,0 +1,8 @@ +createShareLink(); \ No newline at end of file diff --git a/public/api/file/deleteFiles.php b/public/api/file/deleteFiles.php new file mode 100644 index 0000000..83c95c9 --- /dev/null +++ b/public/api/file/deleteFiles.php @@ -0,0 +1,8 @@ +deleteFiles(); \ No newline at end of file diff --git a/public/api/file/deleteTrashFiles.php b/public/api/file/deleteTrashFiles.php new file mode 100644 index 0000000..5f11c7a --- /dev/null +++ b/public/api/file/deleteTrashFiles.php @@ -0,0 +1,8 @@ +deleteTrashFiles(); \ No newline at end of file diff --git a/public/api/file/download.php b/public/api/file/download.php new file mode 100644 index 0000000..d74c427 --- /dev/null +++ b/public/api/file/download.php @@ -0,0 +1,8 @@ +downloadFile(); \ No newline at end of file diff --git a/public/api/file/downloadZip.php b/public/api/file/downloadZip.php new file mode 100644 index 0000000..7d17c85 --- /dev/null +++ b/public/api/file/downloadZip.php @@ -0,0 +1,8 @@ +downloadZip(); \ No newline at end of file diff --git a/public/api/file/extractZip.php b/public/api/file/extractZip.php new file mode 100644 index 0000000..f66efb3 --- /dev/null +++ b/public/api/file/extractZip.php @@ -0,0 +1,8 @@ +extractZip(); \ No newline at end of file diff --git a/public/api/file/getFileList.php b/public/api/file/getFileList.php new file mode 100644 index 0000000..5345663 --- /dev/null +++ b/public/api/file/getFileList.php @@ -0,0 +1,8 @@ +getFileList(); \ No newline at end of file diff --git a/public/api/file/getFileTag.php b/public/api/file/getFileTag.php new file mode 100644 index 0000000..76c0e21 --- /dev/null +++ b/public/api/file/getFileTag.php @@ -0,0 +1,8 @@ +getFileTags(); \ No newline at end of file diff --git a/public/api/file/getTrashItems.php b/public/api/file/getTrashItems.php new file mode 100644 index 0000000..9bcf475 --- /dev/null +++ b/public/api/file/getTrashItems.php @@ -0,0 +1,8 @@ +getTrashItems(); \ No newline at end of file diff --git a/public/api/file/moveFiles.php b/public/api/file/moveFiles.php new file mode 100644 index 0000000..040890c --- /dev/null +++ b/public/api/file/moveFiles.php @@ -0,0 +1,8 @@ +moveFiles(); \ No newline at end of file diff --git a/public/api/file/renameFile.php b/public/api/file/renameFile.php new file mode 100644 index 0000000..a3f5da8 --- /dev/null +++ b/public/api/file/renameFile.php @@ -0,0 +1,8 @@ +renameFile(); \ No newline at end of file diff --git a/public/api/file/restoreFiles.php b/public/api/file/restoreFiles.php new file mode 100644 index 0000000..3584741 --- /dev/null +++ b/public/api/file/restoreFiles.php @@ -0,0 +1,8 @@ +restoreFiles(); \ No newline at end of file diff --git a/public/api/file/saveFile.php b/public/api/file/saveFile.php new file mode 100644 index 0000000..29d1cb5 --- /dev/null +++ b/public/api/file/saveFile.php @@ -0,0 +1,8 @@ +saveFile(); \ No newline at end of file diff --git a/public/api/file/saveFileTag.php b/public/api/file/saveFileTag.php new file mode 100644 index 0000000..87f5ae4 --- /dev/null +++ b/public/api/file/saveFileTag.php @@ -0,0 +1,8 @@ +saveFileTag(); \ No newline at end of file diff --git a/public/api/file/share.php b/public/api/file/share.php new file mode 100644 index 0000000..44971cd --- /dev/null +++ b/public/api/file/share.php @@ -0,0 +1,8 @@ +shareFile(); \ No newline at end of file diff --git a/public/api/file/symlink b/public/api/file/symlink new file mode 100644 index 0000000..a49ca6b --- /dev/null +++ b/public/api/file/symlink @@ -0,0 +1,2 @@ +cd /var/www/public +ln -s ../uploads uploads \ No newline at end of file diff --git a/public/api/folder/createFolder.php b/public/api/folder/createFolder.php new file mode 100644 index 0000000..77defd5 --- /dev/null +++ b/public/api/folder/createFolder.php @@ -0,0 +1,8 @@ +createFolder(); \ No newline at end of file diff --git a/public/api/folder/createShareFolderLink.php b/public/api/folder/createShareFolderLink.php new file mode 100644 index 0000000..126b961 --- /dev/null +++ b/public/api/folder/createShareFolderLink.php @@ -0,0 +1,8 @@ +createShareFolderLink(); \ No newline at end of file diff --git a/public/api/folder/deleteFolder.php b/public/api/folder/deleteFolder.php new file mode 100644 index 0000000..8bcacbc --- /dev/null +++ b/public/api/folder/deleteFolder.php @@ -0,0 +1,8 @@ +deleteFolder(); \ No newline at end of file diff --git a/public/api/folder/downloadSharedFile.php b/public/api/folder/downloadSharedFile.php new file mode 100644 index 0000000..07c69d8 --- /dev/null +++ b/public/api/folder/downloadSharedFile.php @@ -0,0 +1,8 @@ +downloadSharedFile(); \ No newline at end of file diff --git a/public/api/folder/getFolderList.php b/public/api/folder/getFolderList.php new file mode 100644 index 0000000..9c44509 --- /dev/null +++ b/public/api/folder/getFolderList.php @@ -0,0 +1,8 @@ +getFolderList(); \ No newline at end of file diff --git a/public/api/folder/renameFolder.php b/public/api/folder/renameFolder.php new file mode 100644 index 0000000..ce9a344 --- /dev/null +++ b/public/api/folder/renameFolder.php @@ -0,0 +1,8 @@ +renameFolder(); \ No newline at end of file diff --git a/public/api/folder/shareFolder.php b/public/api/folder/shareFolder.php new file mode 100644 index 0000000..6773a38 --- /dev/null +++ b/public/api/folder/shareFolder.php @@ -0,0 +1,8 @@ +shareFolder(); \ No newline at end of file diff --git a/public/api/folder/uploadToSharedFolder.php b/public/api/folder/uploadToSharedFolder.php new file mode 100644 index 0000000..643110f --- /dev/null +++ b/public/api/folder/uploadToSharedFolder.php @@ -0,0 +1,8 @@ +uploadToSharedFolder(); \ No newline at end of file diff --git a/public/api/getUserPermissions.php b/public/api/getUserPermissions.php new file mode 100644 index 0000000..9ebd320 --- /dev/null +++ b/public/api/getUserPermissions.php @@ -0,0 +1,8 @@ +getUserPermissions(); \ No newline at end of file diff --git a/public/api/getUsers.php b/public/api/getUsers.php new file mode 100644 index 0000000..b68da7b --- /dev/null +++ b/public/api/getUsers.php @@ -0,0 +1,8 @@ +getUsers(); // This will output the JSON response \ No newline at end of file diff --git a/public/api/removeUser.php b/public/api/removeUser.php new file mode 100644 index 0000000..350e9de --- /dev/null +++ b/public/api/removeUser.php @@ -0,0 +1,8 @@ +removeUser(); \ No newline at end of file diff --git a/public/api/totp_disable.php b/public/api/totp_disable.php new file mode 100644 index 0000000..a196559 --- /dev/null +++ b/public/api/totp_disable.php @@ -0,0 +1,9 @@ +disableTOTP(); \ No newline at end of file diff --git a/public/api/totp_recover.php b/public/api/totp_recover.php new file mode 100644 index 0000000..32f36b4 --- /dev/null +++ b/public/api/totp_recover.php @@ -0,0 +1,8 @@ +recoverTOTP(); \ No newline at end of file diff --git a/public/api/totp_saveCode.php b/public/api/totp_saveCode.php new file mode 100644 index 0000000..1b2cc95 --- /dev/null +++ b/public/api/totp_saveCode.php @@ -0,0 +1,8 @@ +saveTOTPRecoveryCode(); \ No newline at end of file diff --git a/public/api/totp_setup.php b/public/api/totp_setup.php new file mode 100644 index 0000000..f9b8877 --- /dev/null +++ b/public/api/totp_setup.php @@ -0,0 +1,9 @@ +setupTOTP(); \ No newline at end of file diff --git a/public/api/totp_verify.php b/public/api/totp_verify.php new file mode 100644 index 0000000..fdd48b2 --- /dev/null +++ b/public/api/totp_verify.php @@ -0,0 +1,9 @@ +verifyTOTP(); \ No newline at end of file diff --git a/public/api/updateUserPanel.php b/public/api/updateUserPanel.php new file mode 100644 index 0000000..54be7c4 --- /dev/null +++ b/public/api/updateUserPanel.php @@ -0,0 +1,8 @@ +updateUserPanel(); \ No newline at end of file diff --git a/public/api/updateUserPermissions.php b/public/api/updateUserPermissions.php new file mode 100644 index 0000000..d5406f4 --- /dev/null +++ b/public/api/updateUserPermissions.php @@ -0,0 +1,8 @@ +updateUserPermissions(); \ No newline at end of file diff --git a/public/api/upload/removeChunks.php b/public/api/upload/removeChunks.php new file mode 100644 index 0000000..3fd3edf --- /dev/null +++ b/public/api/upload/removeChunks.php @@ -0,0 +1,8 @@ +removeChunks(); \ No newline at end of file diff --git a/public/api/upload/upload.php b/public/api/upload/upload.php new file mode 100644 index 0000000..195b2cb --- /dev/null +++ b/public/api/upload/upload.php @@ -0,0 +1,7 @@ +handleUpload(); \ No newline at end of file diff --git a/assets/favicon.ico b/public/assets/favicon.ico similarity index 100% rename from assets/favicon.ico rename to public/assets/favicon.ico diff --git a/assets/logo.png b/public/assets/logo.png similarity index 100% rename from assets/logo.png rename to public/assets/logo.png diff --git a/assets/logo.svg b/public/assets/logo.svg similarity index 100% rename from assets/logo.svg rename to public/assets/logo.svg diff --git a/css/styles.css b/public/css/styles.css similarity index 100% rename from css/styles.css rename to public/css/styles.css diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..70f6da1 --- /dev/null +++ b/public/index.html @@ -0,0 +1,485 @@ + + + + + + + FileRise + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

FileRise

+
+
+
+ +
+
+ + + + + + +
+
+
+
+ + +
+ + + +
+ +
+ +
+
+ +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+
Upload Files/Folders
+
+
+
+
+ Drop files/folders here or click 'Choose + Files' +
+ + +
+
+ +
+
+
+
+
+ +
+
+
+ Folder Navigation & Management + +
+
+
+
+
+
+ + + + + + + + +
+ +
+
+
+
+
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/auth.js b/public/js/auth.js similarity index 94% rename from js/auth.js rename to public/js/auth.js index d3401b6..dd99bf2 100644 --- a/js/auth.js +++ b/public/js/auth.js @@ -51,7 +51,7 @@ function openTOTPLoginModal() { const isFormLogin = Boolean(window.__lastLoginData); if (!isFormLogin) { // disable Basic‑Auth link - const basicLink = document.querySelector("a[href='login_basic.php']"); + const basicLink = document.querySelector("a[href='api/auth/login_basic.php']"); if (basicLink) { basicLink.style.pointerEvents = 'none'; basicLink.style.opacity = '0.5'; @@ -78,8 +78,9 @@ function updateItemsPerPageSelect() { function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) { const authForm = document.getElementById("authForm"); + if (authForm) authForm.style.display = disableFormLogin ? "none" : "block"; - const basicAuthLink = document.querySelector("a[href='login_basic.php']"); + const basicAuthLink = document.querySelector("a[href='api/auth/login_basic.php']"); if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block"; const oidcLoginBtn = document.getElementById("oidcLoginBtn"); if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block"; @@ -94,19 +95,17 @@ function updateLoginOptionsUIFromStorage() { } export function loadAdminConfigFunc() { - return fetch("getConfig.php", { credentials: "include" }) + return fetch("api/admin/getConfig.php", { credentials: "include" }) .then(response => response.json()) .then(config => { - // Save header_title into localStorage (if needed) localStorage.setItem("headerTitle", config.header_title || "FileRise"); - // Update login options and global OTPAuth URL as before + // Update login options using the nested loginOptions object. localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin); localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth); localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); - // Update the UI for login options updateLoginOptionsUIFromStorage(); const headerTitleElem = document.querySelector(".header-title h1"); @@ -115,7 +114,7 @@ export function loadAdminConfigFunc() { } }) .catch(() => { - // Fallback defaults in case of error + // Use defaults. localStorage.setItem("headerTitle", "FileRise"); localStorage.setItem("disableFormLogin", "false"); localStorage.setItem("disableBasicAuth", "false"); @@ -214,7 +213,7 @@ function updateAuthenticatedUI(data) { } function checkAuthentication(showLoginToast = true) { - return sendRequest("checkAuth.php") + return sendRequest("api/auth/checkAuth.php") .then(data => { if (data.setup) { window.setupMode = true; @@ -250,12 +249,12 @@ function checkAuthentication(showLoginToast = true) { function submitLogin(data) { setLastLoginData(data); window.__lastLoginData = data; - sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) + sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) .then(response => { if (response.success || response.status === "ok") { sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!"); // Fetch and update permissions, then reload. - sendRequest("getUserPermissions.php", "GET") + sendRequest("api/getUserPermissions.php", "GET") .then(permissionData => { if (permissionData && typeof permissionData === "object") { localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false"); @@ -313,9 +312,11 @@ function closeRemoveUserModal() { } function loadUserList() { - fetch("getUsers.php", { credentials: "include" }) + // Updated path: from "getUsers.php" to "api/getUsers.php" + fetch("api/getUsers.php", { credentials: "include" }) .then(response => response.json()) .then(data => { + // Assuming the endpoint returns an array of users. const users = Array.isArray(data) ? data : (data.users || []); const selectElem = document.getElementById("removeUsernameSelect"); selectElem.innerHTML = ""; @@ -330,7 +331,7 @@ function loadUserList() { closeRemoveUserModal(); } }) - .catch(() => { }); + .catch(() => { /* handle errors if needed */ }); } window.loadUserList = loadUserList; @@ -353,7 +354,7 @@ function initAuth() { }); } document.getElementById("logoutBtn").addEventListener("click", function () { - fetch("logout.php", { + fetch("api/auth/logout.php", { method: "POST", credentials: "include", headers: { "X-CSRF-Token": window.csrfToken } @@ -372,7 +373,7 @@ function initAuth() { showToast("Username and password are required!"); return; } - let url = "addUser.php"; + let url = "api/addUser.php"; if (window.setupMode) url += "?setup=1"; fetch(url, { method: "POST", @@ -407,7 +408,7 @@ function initAuth() { } const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); if (!confirmed) return; - fetch("removeUser.php", { + fetch("api/removeUser.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, @@ -446,7 +447,7 @@ function initAuth() { return; } const data = { oldPassword, newPassword, confirmPassword }; - fetch("changePassword.php", { + fetch("api/changePassword.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, @@ -479,7 +480,7 @@ document.addEventListener("DOMContentLoaded", function () { const oidcLoginBtn = document.getElementById("oidcLoginBtn"); if (oidcLoginBtn) { oidcLoginBtn.addEventListener("click", () => { - window.location.href = "auth.php?oidc=initiate"; + window.location.href = "api/auth/auth.php?oidc=initiate"; }); } diff --git a/js/authModals.js b/public/js/authModals.js similarity index 98% rename from js/authModals.js rename to public/js/authModals.js index 8249092..968a07e 100644 --- a/js/authModals.js +++ b/public/js/authModals.js @@ -84,7 +84,7 @@ export function openTOTPLoginModal() { showToast(t("please_enter_recovery_code")); return; } - fetch("totp_recover.php", { + fetch("api/totp_recover.php", { method: "POST", credentials: "include", headers: { @@ -113,7 +113,7 @@ export function openTOTPLoginModal() { totpInput.addEventListener("input", function () { const code = this.value.trim(); if (code.length === 6) { - fetch("totp_verify.php", { + fetch("api/totp_verify.php", { method: "POST", credentials: "include", headers: { @@ -229,7 +229,7 @@ export function openUserPanel() { totpCheckbox.addEventListener("change", function () { localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false"); const enabled = this.checked; - fetch("updateUserPanel.php", { + fetch("api/updateUserPanel.php", { method: "POST", credentials: "include", headers: { @@ -353,7 +353,7 @@ export function openTOTPModal() { showToast(t("please_enter_valid_code")); return; } - fetch("totp_verify.php", { + fetch("api/totp_verify.php", { method: "POST", credentials: "include", headers: { @@ -367,7 +367,7 @@ export function openTOTPModal() { if (result.status === 'ok') { showToast(t("totp_enabled_successfully")); // After successful TOTP verification, fetch the recovery code - fetch("totp_saveCode.php", { + fetch("api/totp_saveCode.php", { method: "POST", credentials: "include", headers: { @@ -431,7 +431,7 @@ export function openTOTPModal() { } function loadTOTPQRCode() { - fetch("totp_setup.php", { + fetch("api/totp_setup.php", { method: "GET", credentials: "include", headers: { @@ -470,7 +470,7 @@ export function closeTOTPModal(disable = true) { localStorage.setItem("userTOTPEnabled", "false"); } // Call endpoint to remove the TOTP secret from the user's record - fetch("totp_disable.php", { + fetch("api/totp_disable.php", { method: "POST", credentials: "include", headers: { @@ -556,7 +556,7 @@ function showCustomConfirmModal(message) { } export function openAdminPanel() { - fetch("getConfig.php", { credentials: "include" }) + fetch("api/admin/getConfig.php", { credentials: "include" }) .then(response => response.json()) .then(config => { if (config.header_title) { @@ -718,7 +718,7 @@ export function openAdminPanel() { const disableBasicAuth = disableBasicAuthCheckbox.checked; const disableOIDCLogin = disableOIDCLoginCheckbox.checked; const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim(); - sendRequest("updateConfig.php", "POST", { + sendRequest("api/admin/updateConfig.php", "POST", { header_title: newHeaderTitle, oidc: newOIDCConfig, disableFormLogin, @@ -891,7 +891,7 @@ export function openUserPermissionsModal() { }); }); // Send the permissionsData to the server. - sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken }) + sendRequest("api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken }) .then(response => { if (response.success) { showToast(t("user_permissions_updated_successfully")); @@ -917,11 +917,11 @@ function loadUserPermissionsList() { listContainer.innerHTML = ""; // First, fetch the current permissions from the server. - fetch("getUserPermissions.php", { credentials: "include" }) + fetch("api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { // Then, fetch the list of users. - return fetch("getUsers.php", { credentials: "include" }) + return fetch("api/getUsers.php", { credentials: "include" }) .then(response => response.json()) .then(usersData => { const users = Array.isArray(usersData) ? usersData : (usersData.users || []); diff --git a/js/domUtils.js b/public/js/domUtils.js similarity index 100% rename from js/domUtils.js rename to public/js/domUtils.js diff --git a/js/dragAndDrop.js b/public/js/dragAndDrop.js similarity index 100% rename from js/dragAndDrop.js rename to public/js/dragAndDrop.js diff --git a/js/fileActions.js b/public/js/fileActions.js similarity index 97% rename from js/fileActions.js rename to public/js/fileActions.js index cb09080..5a9116a 100644 --- a/js/fileActions.js +++ b/public/js/fileActions.js @@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", function () { const confirmDelete = document.getElementById("confirmDeleteFiles"); if (confirmDelete) { confirmDelete.addEventListener("click", function () { - fetch("deleteFiles.php", { + fetch("api/file/deleteFiles.php", { method: "POST", credentials: "include", headers: { @@ -111,7 +111,7 @@ export function confirmSingleDownload() { // Build the URL for download.php using GET parameters. const folder = window.currentFolder || "root"; - const downloadURL = "download.php?folder=" + encodeURIComponent(folder) + + const downloadURL = "api/file/download.php?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(window.singleFileToDownload); fetch(downloadURL, { @@ -178,7 +178,7 @@ export function handleExtractZipSelected(e) { // Show the progress modal. document.getElementById("downloadProgressModal").style.display = "block"; - fetch("extractZip.php", { + fetch("api/file/extractZip.php", { method: "POST", credentials: "include", headers: { @@ -245,7 +245,7 @@ document.addEventListener("DOMContentLoaded", function () { console.log("Download confirmed. Showing progress modal."); document.getElementById("downloadProgressModal").style.display = "block"; const folder = window.currentFolder || "root"; - fetch("downloadZip.php", { + fetch("api/file/downloadZip.php", { method: "POST", credentials: "include", headers: { @@ -309,7 +309,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) { if (window.userFolderOnly) { const username = localStorage.getItem("username") || "root"; try { - const response = await fetch("getFolderList.php?restricted=1"); + const response = await fetch("api/folder/getFolderList.php?restricted=1"); let folders = await response.json(); if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { folders = folders.map(item => item.folder); @@ -339,7 +339,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) { } try { - const response = await fetch("getFolderList.php"); + const response = await fetch("api/folder/getFolderList.php"); let folders = await response.json(); if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { folders = folders.map(item => item.folder); @@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () { showToast("Error: Cannot copy files to the same folder."); return; } - fetch("copyFiles.php", { + fetch("api/file/copyFiles.php", { method: "POST", credentials: "include", headers: { @@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () { showToast("Error: Cannot move files to the same folder."); return; } - fetch("moveFiles.php", { + fetch("api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -514,7 +514,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } const folderUsed = window.fileFolder; - fetch("renameFile.php", { + fetch("api/file/renameFile.php", { method: "POST", credentials: "include", headers: { diff --git a/js/fileDragDrop.js b/public/js/fileDragDrop.js similarity index 99% rename from js/fileDragDrop.js rename to public/js/fileDragDrop.js index a8bc4fb..9766716 100644 --- a/js/fileDragDrop.js +++ b/public/js/fileDragDrop.js @@ -96,7 +96,7 @@ export function folderDropHandler(event) { return; } if (!dragData || !dragData.fileName) return; - fetch("moveFiles.php", { + fetch("api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { diff --git a/js/fileEditor.js b/public/js/fileEditor.js similarity index 99% rename from js/fileEditor.js rename to public/js/fileEditor.js index 251236a..8c9e88c 100644 --- a/js/fileEditor.js +++ b/public/js/fileEditor.js @@ -160,7 +160,7 @@ export function saveFile(fileName, folder) { content: editor.getValue(), folder: folderUsed }; - fetch("saveFile.php", { + fetch("api/file/saveFile.php", { method: "POST", credentials: "include", headers: { diff --git a/js/fileListView.js b/public/js/fileListView.js similarity index 99% rename from js/fileListView.js rename to public/js/fileListView.js index cbee0b2..7de2fac 100644 --- a/js/fileListView.js +++ b/public/js/fileListView.js @@ -193,7 +193,7 @@ export function loadFileList(folderParam) { fileListContainer.style.visibility = "hidden"; fileListContainer.innerHTML = "
Loading files...
"; - return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) + return fetch("api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) .then(response => { if (response.status === 401) { showToast("Session expired. Please log in again."); diff --git a/js/fileManager.js b/public/js/fileManager.js similarity index 100% rename from js/fileManager.js rename to public/js/fileManager.js diff --git a/js/fileMenu.js b/public/js/fileMenu.js similarity index 100% rename from js/fileMenu.js rename to public/js/fileMenu.js diff --git a/js/filePreview.js b/public/js/filePreview.js similarity index 99% rename from js/filePreview.js rename to public/js/filePreview.js index 0a5b738..123e3b8 100644 --- a/js/filePreview.js +++ b/public/js/filePreview.js @@ -48,7 +48,7 @@ export function openShareModal(file, folder) { document.getElementById("generateShareLinkBtn").addEventListener("click", () => { const expiration = document.getElementById("shareExpiration").value; const password = document.getElementById("sharePassword").value; - fetch("createShareLink.php", { + fetch("api/file/createShareLink.php", { method: "POST", credentials: "include", headers: { @@ -67,7 +67,7 @@ export function openShareModal(file, folder) { if (data.token) { let shareEndpoint = document.querySelector('meta[name="share-url"]') ? document.querySelector('meta[name="share-url"]').getAttribute('content') - : (window.SHARE_URL || "share.php"); + : (window.SHARE_URL || "api/file/share.php"); const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`; const displayDiv = document.getElementById("shareLinkDisplay"); const inputField = document.getElementById("shareLinkInput"); diff --git a/js/fileTags.js b/public/js/fileTags.js similarity index 99% rename from js/fileTags.js rename to public/js/fileTags.js index ea55231..42442b9 100644 --- a/js/fileTags.js +++ b/public/js/fileTags.js @@ -261,7 +261,7 @@ function removeGlobalTag(tagName) { // NEW: Save global tag removal to the server. function saveGlobalTagRemoval(tagName) { - fetch("saveFileTag.php", { + fetch("api/file/saveFileTag.php", { method: "POST", credentials: "include", headers: { @@ -305,7 +305,7 @@ if (localStorage.getItem('globalTags')) { // New function to load global tags from the server's persistent JSON. export function loadGlobalTags() { - fetch("getFileTag.php", { credentials: "include" }) + fetch("api/file/getFileTag.php", { credentials: "include" }) .then(response => { if (!response.ok) { // If the file doesn't exist, assume there are no global tags. @@ -438,7 +438,7 @@ export function saveFileTags(file, deleteGlobal = false, tagToDelete = null) { payload.deleteGlobal = true; payload.tagToDelete = tagToDelete; } - fetch("saveFileTag.php", { + fetch("api/file/saveFileTag.php", { method: "POST", credentials: "include", headers: { diff --git a/js/folderManager.js b/public/js/folderManager.js similarity index 98% rename from js/folderManager.js rename to public/js/folderManager.js index 5d5d101..aca2b9a 100644 --- a/js/folderManager.js +++ b/public/js/folderManager.js @@ -154,7 +154,7 @@ function breadcrumbDropHandler(e) { } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; - fetch("moveFiles.php", { + fetch("api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -202,7 +202,7 @@ function checkUserFolderPermission() { window.currentFolder = username; return Promise.resolve(true); } - return fetch("getUserPermissions.php", { credentials: "include" }) + return fetch("api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { console.log("checkUserFolderPermission: permissionsData =", permissionsData); @@ -302,7 +302,7 @@ function folderDropHandler(event) { } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; - fetch("moveFiles.php", { + fetch("api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -353,7 +353,7 @@ export async function loadFolderTree(selectedFolder) { } // Build fetch URL. - let fetchUrl = 'getFolderList.php'; + let fetchUrl = 'api/folder/getFolderList.php'; if (window.userFolderOnly) { fetchUrl += '?restricted=1'; } @@ -547,7 +547,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function showToast("CSRF token not loaded yet! Please try again."); return; } - fetch("renameFolder.php", { + fetch("api/folder/renameFolder.php", { method: "POST", credentials: "include", headers: { @@ -592,7 +592,7 @@ attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder"); document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("deleteFolder.php", { + fetch("api/folder/deleteFolder.php", { method: "POST", headers: { "Content-Type": "application/json", @@ -639,7 +639,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function fullFolderName = selectedFolder + "/" + folderInput; } const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("createFolder.php", { + fetch("api/folder/createFolder.php", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/js/folderShareModal.js b/public/js/folderShareModal.js similarity index 98% rename from js/folderShareModal.js rename to public/js/folderShareModal.js index fa24606..f2d46c9 100644 --- a/js/folderShareModal.js +++ b/public/js/folderShareModal.js @@ -64,7 +64,7 @@ export function openFolderShareModal(folder) { return; } // Post to the createFolderShareLink endpoint. - fetch("/createFolderShareLink.php", { + fetch("api/folder/createShareFolderLink.php", { method: "POST", credentials: "include", headers: { diff --git a/js/i18n.js b/public/js/i18n.js similarity index 100% rename from js/i18n.js rename to public/js/i18n.js diff --git a/js/main.js b/public/js/main.js similarity index 99% rename from js/main.js rename to public/js/main.js index 671d0f4..5875a9d 100644 --- a/js/main.js +++ b/public/js/main.js @@ -14,7 +14,7 @@ import { t, applyTranslations, setLocale } from './i18n.js'; // Remove the retry logic version and just use loadCsrfToken directly: function loadCsrfToken() { - return fetch('token.php', { credentials: 'include' }) + return fetch('api/auth/token.php', { credentials: 'include' }) .then(response => { if (!response.ok) { throw new Error("Token fetch failed with status: " + response.status); diff --git a/js/networkUtils.js b/public/js/networkUtils.js similarity index 100% rename from js/networkUtils.js rename to public/js/networkUtils.js diff --git a/js/trashRestoreDelete.js b/public/js/trashRestoreDelete.js similarity index 96% rename from js/trashRestoreDelete.js rename to public/js/trashRestoreDelete.js index 4749a86..d889a49 100644 --- a/js/trashRestoreDelete.js +++ b/public/js/trashRestoreDelete.js @@ -69,7 +69,7 @@ export function setupTrashRestoreDelete() { showToast(t("no_trash_selected")); return; } - fetch("restoreFiles.php", { + fetch("api/file/restoreFiles.php", { method: "POST", credentials: "include", headers: { @@ -109,7 +109,7 @@ export function setupTrashRestoreDelete() { showToast(t("trash_empty")); return; } - fetch("restoreFiles.php", { + fetch("api/file/restoreFiles.php", { method: "POST", credentials: "include", headers: { @@ -151,7 +151,7 @@ export function setupTrashRestoreDelete() { return; } showConfirm("Are you sure you want to permanently delete the selected trash items?", () => { - fetch("deleteTrashFiles.php", { + fetch("api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { @@ -186,7 +186,7 @@ export function setupTrashRestoreDelete() { if (deleteAllBtn) { deleteAllBtn.addEventListener("click", () => { showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => { - fetch("deleteTrashFiles.php", { + fetch("api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { @@ -234,7 +234,7 @@ export function setupTrashRestoreDelete() { * Loads trash items from the server and updates the restore modal list. */ export function loadTrashItems() { - fetch("getTrashItems.php", { credentials: "include" }) + fetch("api/file/getTrashItems.php", { credentials: "include" }) .then(response => response.json()) .then(trashItems => { const listContainer = document.getElementById("restoreFilesList"); @@ -271,7 +271,7 @@ export function loadTrashItems() { * Automatically purges (permanently deletes) trash items older than 3 days. */ function autoPurgeOldTrash() { - fetch("getTrashItems.php", { credentials: "include" }) + fetch("api/file/getTrashItems.php", { credentials: "include" }) .then(response => response.json()) .then(trashItems => { const now = Date.now(); @@ -279,7 +279,7 @@ function autoPurgeOldTrash() { const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays); if (oldItems.length > 0) { const files = oldItems.map(item => item.trashName); - fetch("deleteTrashFiles.php", { + fetch("api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { diff --git a/js/upload.js b/public/js/upload.js similarity index 99% rename from js/upload.js rename to public/js/upload.js index 947a807..c3d72fe 100644 --- a/js/upload.js +++ b/public/js/upload.js @@ -126,7 +126,7 @@ function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, int // Prefix with "resumable_" to match your PHP regex. params.append('folder', 'resumable_' + identifier); params.append('csrf_token', csrfToken); - fetch('removeChunks.php', { + fetch('api/upload/removeChunks.php', { method: 'POST', credentials: 'include', headers: { @@ -405,7 +405,7 @@ const useResumable = true; // Enable resumable for file picker uploads let resumableInstance; function initResumableUpload() { resumableInstance = new Resumable({ - target: "upload.php", + target: "api/upload/upload.php", query: { folder: window.currentFolder || "root", upload_token: window.csrfToken }, chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks simultaneousUploads: 3, @@ -664,7 +664,7 @@ function submitFiles(allFiles) { } }); - xhr.open("POST", "upload.php", true); + xhr.open("POST", "api/upload/upload.php", true); xhr.setRequestHeader("X-CSRF-Token", window.csrfToken); xhr.send(formData); }); diff --git a/removeChunks.php b/removeChunks.php deleted file mode 100644 index 44f608a..0000000 --- a/removeChunks.php +++ /dev/null @@ -1,53 +0,0 @@ - "Invalid CSRF token"]); - exit; -} - -if (!isset($_POST['folder'])) { - http_response_code(400); - echo json_encode(["error" => "No folder specified"]); - exit; -} - -$folder = urldecode($_POST['folder']); -// The folder name should match the "resumable_" pattern exactly. -$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; -if (!preg_match($regex, $folder)) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder name"]); - exit; -} - -$tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; -if (!is_dir($tempDir)) { - echo json_encode(["success" => true, "message" => "Temporary folder already removed."]); - exit; -} - -function rrmdir($dir) { - if (!is_dir($dir)) return; - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($it as $file) { - $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); - } - rmdir($dir); -} - -rrmdir($tempDir); - -if (!is_dir($tempDir)) { - echo json_encode(["success" => true, "message" => "Temporary folder removed."]); -} else { - http_response_code(500); - echo json_encode(["error" => "Failed to remove temporary folder."]); -} -?> \ No newline at end of file diff --git a/removeUser.php b/removeUser.php deleted file mode 100644 index 7289ebe..0000000 --- a/removeUser.php +++ /dev/null @@ -1,88 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Only allow admins to remove users -if ( - !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || - !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true -) { - echo json_encode(["error" => "Unauthorized"]); - exit; -} - -// Get input data from JSON -$data = json_decode(file_get_contents("php://input"), true); -$usernameToRemove = trim($data["username"] ?? ""); - -if (!$usernameToRemove) { - echo json_encode(["error" => "Username is required"]); - exit; -} - -// Optional: Validate the username format (allow letters, numbers, underscores, dashes, and spaces) -if (!preg_match(REGEX_USER, $usernameToRemove)) { - echo json_encode(["error" => "Invalid username format"]); - exit; -} - -// Prevent removal of the currently logged-in user -if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) { - echo json_encode(["error" => "Cannot remove yourself"]); - exit; -} - -// Read existing users from the file -if (!file_exists($usersFile)) { - echo json_encode(["error" => "Users file not found"]); - exit; -} - -$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); -$newUsers = []; -$userFound = false; - -// Remove the user with the specified username -foreach ($existingUsers as $line) { - $parts = explode(':', trim($line)); - if (count($parts) < 3) { - continue; - } - $storedUser = $parts[0]; - if ($storedUser === $usernameToRemove) { - $userFound = true; - continue; // Skip this user - } - $newUsers[] = $line; -} - -if (!$userFound) { - echo json_encode(["error" => "User not found"]); - exit; -} - -// Write the updated list back to users.txt -file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL); - -// Also update the userPermissions.json file -$permissionsFile = USERS_DIR . "userPermissions.json"; -if (file_exists($permissionsFile)) { - $permissionsJson = file_get_contents($permissionsFile); - $permissionsArray = json_decode($permissionsJson, true); - if (is_array($permissionsArray) && isset($permissionsArray[$usernameToRemove])) { - unset($permissionsArray[$usernameToRemove]); - file_put_contents($permissionsFile, json_encode($permissionsArray, JSON_PRETTY_PRINT)); - } -} - -echo json_encode(["success" => "User removed successfully"]); -?> \ No newline at end of file diff --git a/renameFile.php b/renameFile.php deleted file mode 100644 index 4bb32f6..0000000 --- a/renameFile.php +++ /dev/null @@ -1,114 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to rename files."]); - exit(); - } -} - -$data = json_decode(file_get_contents("php://input"), true); -if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) { - echo json_encode(["error" => "Invalid input"]); - exit; -} - -$folder = trim($data['folder']) ?: 'root'; -// For subfolders, allow letters, numbers, underscores, dashes, spaces, and forward slashes. -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name"]); - exit; -} - -$oldName = basename(trim($data['oldName'])); -$newName = basename(trim($data['newName'])); - -// Validate file names: allow letters, numbers, underscores, dashes, dots, parentheses, and spaces. -if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) { - echo json_encode(["error" => "Invalid file name."]); - exit; -} - -// Determine the directory path based on the folder. -if ($folder !== 'root') { - $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; -} else { - $directory = UPLOAD_DIR; -} - -$oldPath = $directory . $oldName; -$newPath = $directory . $newName; - -// Helper: Generate a unique file name if a file with the same name exists. -function getUniqueFileName($directory, $fileName) { - $fullPath = $directory . $fileName; - clearstatcache(true, $fullPath); - if (!file_exists($fullPath)) { - return $fileName; - } - $basename = pathinfo($fileName, PATHINFO_FILENAME); - $extension = pathinfo($fileName, PATHINFO_EXTENSION); - $counter = 1; - do { - $newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : ""); - $newFullPath = $directory . $newName; - clearstatcache(true, $newFullPath); - $counter++; - } while (file_exists($directory . $newName)); - return $newName; -} - -if (!file_exists($oldPath)) { - echo json_encode(["error" => "File does not exist"]); - exit; -} - -// If a file with the new name exists, generate a unique name. -if (file_exists($newPath)) { - $newName = getUniqueFileName($directory, $newName); - $newPath = $directory . $newName; -} - -if (rename($oldPath, $newPath)) { - // --- Update Metadata in the Folder-Specific JSON --- - $metadataKey = ($folder === 'root') ? "root" : $folder; - $metadataFile = META_DIR . str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; - - if (file_exists($metadataFile)) { - $metadata = json_decode(file_get_contents($metadataFile), true); - if (isset($metadata[$oldName])) { - $metadata[$newName] = $metadata[$oldName]; - unset($metadata[$oldName]); - file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); - } - } - echo json_encode(["success" => "File renamed successfully", "newName" => $newName]); -} else { - echo json_encode(["error" => "Error renaming file"]); -} -?> \ No newline at end of file diff --git a/renameFolder.php b/renameFolder.php deleted file mode 100644 index 142a492..0000000 --- a/renameFolder.php +++ /dev/null @@ -1,98 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -// Ensure the request method is POST -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - echo json_encode(['success' => false, 'error' => 'Invalid request method.']); - exit; -} - -// CSRF Protection: Read token from the custom header "X-CSRF-Token" -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); - http_response_code(403); - exit; -} -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to rename folders."]); - exit(); - } -} - -// Get the JSON input and decode it -$input = json_decode(file_get_contents('php://input'), true); -if (!isset($input['oldFolder']) || !isset($input['newFolder'])) { - echo json_encode(['success' => false, 'error' => 'Required folder names not provided.']); - exit; -} - -$oldFolder = trim($input['oldFolder']); -$newFolder = trim($input['newFolder']); - -// Validate folder names -if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) { - echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']); - exit; -} - -$oldFolder = trim($oldFolder, "/\\ "); -$newFolder = trim($newFolder, "/\\ "); - -$baseDir = rtrim(UPLOAD_DIR, '/\\'); -$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder; -$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder; - -if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) || - strpos(realpath($oldPath), realpath($baseDir)) !== 0 || - strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) { - echo json_encode(['success' => false, 'error' => 'Invalid folder path.']); - exit; -} - -if (!file_exists($oldPath) || !is_dir($oldPath)) { - echo json_encode(['success' => false, 'error' => 'Folder to rename does not exist.']); - exit; -} - -if (file_exists($newPath)) { - echo json_encode(['success' => false, 'error' => 'New folder name already exists.']); - exit; -} - -// Attempt to rename the folder. -if (rename($oldPath, $newPath)) { - // --- Update Metadata Files --- - // Generate a metadata prefix for the old folder path and new folder path. - $oldPrefix = str_replace(['/', '\\', ' '], '-', $oldFolder); - $newPrefix = str_replace(['/', '\\', ' '], '-', $newFolder); - - // Find all metadata files whose names start with the old prefix. - $metadataFiles = glob(META_DIR . $oldPrefix . '*_metadata.json'); - foreach ($metadataFiles as $oldMetaFile) { - $baseName = basename($oldMetaFile); - // Replace the old prefix with the new prefix in the filename. - $newBaseName = preg_replace('/^' . preg_quote($oldPrefix, '/') . '/', $newPrefix, $baseName); - $newMetaFile = META_DIR . $newBaseName; - rename($oldMetaFile, $newMetaFile); - } - - echo json_encode(['success' => true]); -} else { - echo json_encode(['success' => false, 'error' => 'Failed to rename folder.']); -} -?> \ No newline at end of file diff --git a/restoreFiles.php b/restoreFiles.php deleted file mode 100644 index 23d3858..0000000 --- a/restoreFiles.php +++ /dev/null @@ -1,175 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} - -// Define the trash directory and trash metadata file. -$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; -if (!file_exists($trashDir)) { - mkdir($trashDir, 0755, true); -} -$trashMetadataFile = $trashDir . "trash.json"; -$trashData = []; -if (file_exists($trashMetadataFile)) { - $json = file_get_contents($trashMetadataFile); - $trashData = json_decode($json, true); - if (!is_array($trashData)) { - $trashData = []; - } -} - -// Helper: Generate the metadata file path for a given folder. -// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -// Read request body. -$data = json_decode(file_get_contents("php://input"), true); - -// Validate request. -if (!isset($data['files']) || !is_array($data['files'])) { - echo json_encode(["error" => "No file or folder identifiers provided"]); - exit; -} - -// Define a safe file name pattern. -$safeFileNamePattern = REGEX_FILE_NAME; - -$restoredItems = []; -$errors = []; - -foreach ($data['files'] as $trashFileName) { - $trashFileName = trim($trashFileName); - if (!preg_match($safeFileNamePattern, $trashFileName)) { - $errors[] = "$trashFileName has an invalid format."; - continue; - } - - // Find the matching trash record. - $recordKey = null; - foreach ($trashData as $key => $record) { - if (isset($record['trashName']) && $record['trashName'] === $trashFileName) { - $recordKey = $key; - break; - } - } - if ($recordKey === null) { - $errors[] = "No trash record found for $trashFileName."; - continue; - } - - $record = $trashData[$recordKey]; - if (!isset($record['originalFolder']) || !isset($record['originalName'])) { - $errors[] = "Incomplete trash record for $trashFileName."; - continue; - } - $originalFolder = $record['originalFolder']; - $originalName = $record['originalName']; - - // Convert the absolute original folder to a relative folder. - $relativeFolder = 'root'; - if (strpos($originalFolder, UPLOAD_DIR) === 0) { - $relativeFolder = trim(substr($originalFolder, strlen(UPLOAD_DIR)), '/\\'); - if ($relativeFolder === '') { - $relativeFolder = 'root'; - } - } - - // Build destination path. - if ($relativeFolder !== 'root') { - $destinationPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $relativeFolder . DIRECTORY_SEPARATOR . $originalName; - } else { - $destinationPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $originalName; - } - - // If the record is for a folder, recreate the folder. - if (isset($record['type']) && $record['type'] === 'folder') { - if (!file_exists($destinationPath)) { - if (mkdir($destinationPath, 0755, true)) { - $restoredItems[] = $originalName . " (folder restored)"; - } else { - $errors[] = "Failed to restore folder $originalName."; - continue; - } - } else { - $errors[] = "Folder already exists at destination: $originalName."; - continue; - } - // Remove the trash record and continue. - unset($trashData[$recordKey]); - continue; - } - - // For files: Ensure the destination directory exists. - $destinationDir = dirname($destinationPath); - if (!file_exists($destinationDir)) { - if (!mkdir($destinationDir, 0755, true)) { - $errors[] = "Failed to create destination folder for $originalName."; - continue; - } - } - - if (file_exists($destinationPath)) { - $errors[] = "File already exists at destination: $originalName."; - continue; - } - - // Move the file from trash to its original location. - $sourcePath = $trashDir . $trashFileName; - if (file_exists($sourcePath)) { - if (rename($sourcePath, $destinationPath)) { - $restoredItems[] = $originalName; - // Update metadata for the restored file. - $metadataFile = getMetadataFilePath($relativeFolder); - $metadata = []; - if (file_exists($metadataFile)) { - $metadata = json_decode(file_get_contents($metadataFile), true); - if (!is_array($metadata)) { - $metadata = []; - } - } - $restoredMeta = [ - "uploaded" => isset($record['uploaded']) ? $record['uploaded'] : date(DATE_TIME_FORMAT), - "uploader" => isset($record['uploader']) ? $record['uploader'] : "Unknown" - ]; - $metadata[$originalName] = $restoredMeta; - file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); - unset($trashData[$recordKey]); - } else { - $errors[] = "Failed to restore $originalName."; - } - } else { - $errors[] = "Trash file not found: $trashFileName."; - } -} - -// Write back updated trash metadata. -file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); - -if (empty($errors)) { - echo json_encode(["success" => "Items restored: " . implode(", ", $restoredItems)]); -} else { - echo json_encode(["error" => implode("; ", $errors) . ". Items restored: " . implode(", ", $restoredItems)]); -} -exit; -?> \ No newline at end of file diff --git a/saveFile.php b/saveFile.php deleted file mode 100644 index 58b33c2..0000000 --- a/saveFile.php +++ /dev/null @@ -1,121 +0,0 @@ - "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -// Ensure user is authenticated -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); - http_response_code(401); - exit; -} -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to save files."]); - exit(); - } -} - -$data = json_decode(file_get_contents("php://input"), true); - -// Debugging: Check what data is received. -if (!$data) { - echo json_encode(["error" => "No data received"]); - exit; -} - -if (!isset($data["fileName"]) || !isset($data["content"])) { - echo json_encode(["error" => "Invalid request data", "received" => $data]); - exit; -} - -$fileName = basename($data["fileName"]); - -// Determine the folder. Default to "root" if not provided. -$folder = isset($data["folder"]) ? trim($data["folder"]) : "root"; - -// If a subfolder is provided, validate it. -// Allow letters, numbers, underscores, dashes, spaces, and forward slashes. -if ($folder !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name"]); - exit; -} - -// Trim any leading/trailing slashes or spaces. -$folder = trim($folder, "/\\ "); - -// Determine the target upload directory. -$baseDir = rtrim(UPLOAD_DIR, '/\\'); -if ($folder && strtolower($folder) !== "root") { - $targetDir = $baseDir . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; -} else { - $targetDir = $baseDir . DIRECTORY_SEPARATOR; -} - -// (Optional security check: Ensure $targetDir starts with $baseDir) -if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) { - echo json_encode(["error" => "Invalid folder path"]); - exit; -} - -if (!is_dir($targetDir)) { - mkdir($targetDir, 0775, true); -} - -$filePath = $targetDir . $fileName; - -// Attempt to save the file. -if (file_put_contents($filePath, $data["content"]) !== false) { - - // --- Update Metadata (Using Separate JSON per Folder) --- - // Determine the metadata key: for "root", use "root"; otherwise, use the folder path. - $metadataKey = (strtolower($folder) === "root" || $folder === "") ? "root" : $folder; - // Create a unique metadata filename by replacing slashes, backslashes, and spaces with dashes. - $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; - $metadataFilePath = META_DIR . $metadataFileName; - - // Load existing metadata for this folder if it exists. - if (file_exists($metadataFilePath)) { - $metadata = json_decode(file_get_contents($metadataFilePath), true); - } else { - $metadata = []; - } - - $currentTime = date(DATE_TIME_FORMAT); - $uploader = $_SESSION['username'] ?? "Unknown"; - - // Update metadata for this file. - // If the file already exists in metadata, update its "modified" field. - if (isset($metadata[$fileName])) { - $metadata[$fileName]['modified'] = $currentTime; - // Optionally, you might also update the uploader if desired. - $metadata[$fileName]['uploader'] = $uploader; - } else { - // New entry: record both uploaded and modified times. - $metadata[$fileName] = [ - "uploaded" => $currentTime, - "modified" => $currentTime, - "uploader" => $uploader - ]; - } - - // Save the updated metadata. - file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)); - - echo json_encode(["success" => "File saved successfully"]); -} else { - echo json_encode(["error" => "Error saving file"]); -} -?> \ No newline at end of file diff --git a/saveFileTag.php b/saveFileTag.php deleted file mode 100644 index 63e36a6..0000000 --- a/saveFileTag.php +++ /dev/null @@ -1,148 +0,0 @@ - "Unauthorized"]); - http_response_code(401); - exit; -} - -// CSRF Protection: validate token from header. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - respond('error', 403, 'Invalid CSRF token'); -} - -$username = $_SESSION['username'] ?? ''; -$userPermissions = loadUserPermissions($username); -if ($username) { - $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { - echo json_encode(["error" => "Read-only users are not allowed to file tags"]); - exit(); - } -} - -// Retrieve and sanitize input. -$data = json_decode(file_get_contents('php://input'), true); -$file = isset($data['file']) ? trim($data['file']) : ''; -$folder = isset($data['folder']) ? trim($data['folder']) : 'root'; -$tags = isset($data['tags']) ? $data['tags'] : []; - -// Basic validation. -if ($file === '') { - echo json_encode(["error" => "No file specified."]); - exit; -} - -$globalTagsFile = META_DIR . "createdTags.json"; - -// If file is "global", update the global tags only. -if ($file === "global") { - if (!file_exists($globalTagsFile)) { - if (file_put_contents($globalTagsFile, json_encode([], JSON_PRETTY_PRINT)) === false) { - echo json_encode(["error" => "Failed to create global tags file."]); - exit; - } - } - $globalTags = json_decode(file_get_contents($globalTagsFile), true); - if (!is_array($globalTags)) { - $globalTags = []; - } - // If deleteGlobal flag is set and tagToDelete is provided, remove it. - if (isset($data['deleteGlobal']) && $data['deleteGlobal'] === true && isset($data['tagToDelete'])) { - $tagToDelete = strtolower($data['tagToDelete']); - $globalTags = array_values(array_filter($globalTags, function($globalTag) use ($tagToDelete) { - return strtolower($globalTag['name']) !== $tagToDelete; - })); - } else { - // Otherwise, merge new tags. - foreach ($tags as $tag) { - $found = false; - foreach ($globalTags as &$globalTag) { - if (strtolower($globalTag['name']) === strtolower($tag['name'])) { - $globalTag['color'] = $tag['color']; - $found = true; - break; - } - } - if (!$found) { - $globalTags[] = $tag; - } - } - } - if (file_put_contents($globalTagsFile, json_encode($globalTags, JSON_PRETTY_PRINT)) === false) { - echo json_encode(["error" => "Failed to save global tags."]); - exit; - } - echo json_encode(["success" => "Global tags updated successfully.", "globalTags" => $globalTags]); - exit; -} - -// Validate folder name. -if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - echo json_encode(["error" => "Invalid folder name."]); - exit; -} - -function getMetadataFilePath($folder) { - if (strtolower($folder) === 'root' || $folder === '') { - return META_DIR . "root_metadata.json"; - } - return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; -} - -$metadataFile = getMetadataFilePath($folder); -$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; - -if (!isset($metadata[$file])) { - $metadata[$file] = []; -} -$metadata[$file]['tags'] = $tags; - -if (file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)) === false) { - echo json_encode(["error" => "Failed to save tag data."]); - exit; -} - -// Now update the global tags file as well. -if (!file_exists($globalTagsFile)) { - if (file_put_contents($globalTagsFile, json_encode([], JSON_PRETTY_PRINT)) === false) { - echo json_encode(["error" => "Failed to create global tags file."]); - exit; - } -} - -$globalTags = json_decode(file_get_contents($globalTagsFile), true); -if (!is_array($globalTags)) { - $globalTags = []; -} - -foreach ($tags as $tag) { - $found = false; - foreach ($globalTags as &$globalTag) { - if (strtolower($globalTag['name']) === strtolower($tag['name'])) { - $globalTag['color'] = $tag['color']; - $found = true; - break; - } - } - if (!$found) { - $globalTags[] = $tag; - } -} - -if (file_put_contents($globalTagsFile, json_encode($globalTags, JSON_PRETTY_PRINT)) === false) { - echo json_encode(["error" => "Failed to save global tags."]); - exit; -} - -echo json_encode(["success" => "Tag data saved successfully.", "tags" => $tags, "globalTags" => $globalTags]); -?> \ No newline at end of file diff --git a/share.php b/share.php deleted file mode 100644 index 948cda6..0000000 --- a/share.php +++ /dev/null @@ -1,149 +0,0 @@ - "Missing token."]); - exit; -} - -// Load share links from file -$shareFile = META_DIR . "share_links.json"; -if (!file_exists($shareFile)) { - http_response_code(404); - echo json_encode(["error" => "Share link not found."]); - exit; -} - -$shareLinks = json_decode(file_get_contents($shareFile), true); -if (!is_array($shareLinks) || !isset($shareLinks[$token])) { - http_response_code(404); - echo json_encode(["error" => "Share link not found."]); - exit; -} - -$record = $shareLinks[$token]; - -// Check expiration. -if (time() > $record['expires']) { - http_response_code(403); - echo json_encode(["error" => "This link has expired."]); - exit; -} - -// If a password is required and none is provided, show a password form. -if (!empty($record['password']) && empty($providedPass)) { - ?> - - - - - - Enter Password - - - -

This file is protected by a password.

-
- - - - -
- - - "Invalid password."]); - exit; - } -} - -// Build file path securely. -$folder = trim($record['folder'], "/\\ "); -$file = $record['file']; -$filePath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; -if (!empty($folder) && strtolower($folder) !== 'root') { - $filePath .= $folder . DIRECTORY_SEPARATOR; -} -$filePath .= $file; - -// Resolve the real path and ensure it's within the allowed directory. -$realFilePath = realpath($filePath); -$uploadDirReal = realpath(UPLOAD_DIR); -if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) { - http_response_code(404); - echo json_encode(["error" => "File not found."]); - exit; -} - -if (!file_exists($realFilePath)) { - http_response_code(404); - echo json_encode(["error" => "File not found."]); - exit; -} - -// Serve the file. -$mimeType = mime_content_type($realFilePath); -header("Content-Type: " . $mimeType); - -// Set Content-Disposition based on file type. -$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); -if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) { - header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); -} else { - header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); -} - -// Optionally disable caching for sensitive files. -header("Cache-Control: no-store, no-cache, must-revalidate"); -header("Pragma: no-cache"); - -readfile($realFilePath); -exit; -?> \ No newline at end of file diff --git a/shareFolder.php b/shareFolder.php deleted file mode 100644 index f260f32..0000000 --- a/shareFolder.php +++ /dev/null @@ -1,457 +0,0 @@ - "Missing token."]); - exit; -} - -// Load share folder records securely. -$shareFile = META_DIR . "share_folder_links.json"; -if (!file_exists($shareFile)) { - http_response_code(404); - echo json_encode(["error" => "Share link not found."]); - exit; -} - -$shareLinks = json_decode(file_get_contents($shareFile), true); -if (!is_array($shareLinks) || !isset($shareLinks[$token])) { - http_response_code(404); - echo json_encode(["error" => "Share link not found."]); - exit; -} - -$record = $shareLinks[$token]; - -// Check expiration. -if (time() > $record['expires']) { - http_response_code(403); - echo json_encode(["error" => "This link has expired."]); - exit; -} - -// If password protection is enabled and no password is provided, show password form. -if (!empty($record['password']) && empty($providedPass)) { - ?> - - - - - Enter Password - - - -
-

Folder Protected

-

This folder is protected by a password.

-
- - - - -
-
- - - "Invalid password."]); - exit; - } -} - -// Determine the folder path. -$folder = trim($record['folder'], "/\\ "); -$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; -$realFolderPath = realpath($folderPath); -$uploadDirReal = realpath(UPLOAD_DIR); - -// Validate that the folder exists and is within UPLOAD_DIR. -if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { - http_response_code(404); - echo json_encode(["error" => "Folder not found."]); - exit; -} - -// Scan and sort files. -$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) { - return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item); -})); -sort($allFiles); - -// Pagination variables — limits the number of files (and thus images) per page. -$itemsPerPage = 10; -$totalFiles = count($allFiles); -$totalPages = max(1, ceil($totalFiles / $itemsPerPage)); -$currentPage = min($page, $totalPages); -$startIndex = ($currentPage - 1) * $itemsPerPage; -$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage); - -/** - * Convert file size in bytes into a human-readable string. - * - * @param int $bytes The file size in bytes. - * @return string The formatted size string. - */ -function formatBytes($bytes) { - if ($bytes < 1024) { - return $bytes . " B"; - } elseif ($bytes < 1024 * 1024) { - return round($bytes / 1024, 2) . " KB"; - } elseif ($bytes < 1024 * 1024 * 1024) { - return round($bytes / (1024 * 1024), 2) . " MB"; - } else { - return round($bytes / (1024 * 1024 * 1024), 2) . " GB"; - } -} -?> - - - - - Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?> - - - - -
-

Shared Folder:

-
-
- - - - -
- -

This folder is empty.

- - - - - - - - - - - - - - - - -
FilenameSize
- - - - -
- -
- - - - - - - - -
-

Upload File (50mb max size)

-
- - - -

- -
-
- -
- - - - - \ No newline at end of file diff --git a/src/controllers/adminController.php b/src/controllers/adminController.php new file mode 100644 index 0000000..6df3651 --- /dev/null +++ b/src/controllers/adminController.php @@ -0,0 +1,210 @@ + 'Unauthorized access.']); + exit; + } + + // Validate CSRF token. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token.']); + exit; + } + + // Retrieve and decode JSON input. + $input = file_get_contents('php://input'); + $data = json_decode($input, true); + if (!is_array($data)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid input.']); + exit; + } + + // Prepare configuration array. + $headerTitle = isset($data['header_title']) ? trim($data['header_title']) : ""; + $oidc = isset($data['oidc']) ? $data['oidc'] : []; + $oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : ''; + $oidcClientId = isset($oidc['clientId']) ? trim($oidc['clientId']) : ''; + $oidcClientSecret = isset($oidc['clientSecret']) ? trim($oidc['clientSecret']) : ''; + $oidcRedirectUri = isset($oidc['redirectUri']) ? filter_var($oidc['redirectUri'], FILTER_SANITIZE_URL) : ''; + if (!$oidcProviderUrl || !$oidcClientId || !$oidcClientSecret || !$oidcRedirectUri) { + http_response_code(400); + echo json_encode(['error' => 'Incomplete OIDC configuration.']); + exit; + } + + $disableFormLogin = false; + if (isset($data['loginOptions']['disableFormLogin'])) { + $disableFormLogin = filter_var($data['loginOptions']['disableFormLogin'], FILTER_VALIDATE_BOOLEAN); + } elseif (isset($data['disableFormLogin'])) { + $disableFormLogin = filter_var($data['disableFormLogin'], FILTER_VALIDATE_BOOLEAN); + } + $disableBasicAuth = false; + if (isset($data['loginOptions']['disableBasicAuth'])) { + $disableBasicAuth = filter_var($data['loginOptions']['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN); + } elseif (isset($data['disableBasicAuth'])) { + $disableBasicAuth = filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN); + } + + $disableOIDCLogin = false; + if (isset($data['loginOptions']['disableOIDCLogin'])) { + $disableOIDCLogin = filter_var($data['loginOptions']['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN); + } elseif (isset($data['disableOIDCLogin'])) { + $disableOIDCLogin = filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN); + } + $globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : ""; + + $configUpdate = [ + 'header_title' => $headerTitle, + 'oidc' => [ + 'providerUrl' => $oidcProviderUrl, + 'clientId' => $oidcClientId, + 'clientSecret' => $oidcClientSecret, + 'redirectUri' => $oidcRedirectUri, + ], + 'loginOptions' => [ + 'disableFormLogin' => $disableFormLogin, + 'disableBasicAuth' => $disableBasicAuth, + 'disableOIDCLogin' => $disableOIDCLogin, + ], + 'globalOtpauthUrl' => $globalOtpauthUrl + ]; + + // Delegate to the model. + $result = AdminModel::updateConfig($configUpdate); + if (isset($result['error'])) { + http_response_code(500); + } + echo json_encode($result); + exit; + } +} diff --git a/src/controllers/authController.php b/src/controllers/authController.php new file mode 100644 index 0000000..ae01c52 --- /dev/null +++ b/src/controllers/authController.php @@ -0,0 +1,524 @@ +getMessage()); + http_response_code(500); + echo json_encode(["error" => "Internal Server Error"]); + exit(); + }); + + header('Content-Type: application/json'); + + // If OIDC parameters are present, initiate OIDC flow. + $oidcAction = $_GET['oidc'] ?? null; + if (!$oidcAction && isset($_GET['code'])) { + $oidcAction = 'callback'; + } + if ($oidcAction) { + // Load admin configuration for OIDC. + $adminConfigFile = USERS_DIR . 'adminConfig.json'; + if (file_exists($adminConfigFile)) { + $enc = file_get_contents($adminConfigFile); + $dec = decryptData($enc, $encryptionKey); + $cfg = ($dec !== false) ? json_decode($dec, true) : []; + } else { + $cfg = []; + } + $oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com'; + $oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID'; + $oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET'; + $oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/api/auth/auth.php?oidc=callback'; + + $oidc = new OpenIDConnectClient($oidc_provider_url, $oidc_client_id, $oidc_client_secret); + $oidc->setRedirectURL($oidc_redirect_uri); + + if ($oidcAction === 'callback') { + try { + $oidc->authenticate(); + $username = $oidc->requestUserInfo('preferred_username'); + + // Check for TOTP secret. + $totp_secret = null; + $usersFile = USERS_DIR . USERS_FILE; + if (file_exists($usersFile)) { + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(":", trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + $totp_secret = decryptData($parts[3], $encryptionKey); + break; + } + } + } + if ($totp_secret) { + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $totp_secret; + header("Location: index.html?totp_required=1"); + exit(); + } + + // Finalize login (no TOTP) + session_regenerate_id(true); + $_SESSION["authenticated"] = true; + $_SESSION["username"] = $username; + $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); + $_SESSION["folderOnly"] = loadUserPermissions($username); + header("Location: index.html"); + exit(); + } catch (Exception $e) { + error_log("OIDC authentication error: " . $e->getMessage()); + http_response_code(401); + echo json_encode(["error" => "Authentication failed."]); + exit(); + } + } else { + // Initiate OIDC authentication. + try { + $oidc->authenticate(); + exit(); + } catch (Exception $e) { + error_log("OIDC initiation error: " . $e->getMessage()); + http_response_code(401); + echo json_encode(["error" => "Authentication initiation failed."]); + exit(); + } + } + } + + // Fallback: Form-based Authentication. + $data = json_decode(file_get_contents("php://input"), true); + $username = trim($data["username"] ?? ""); + $password = trim($data["password"] ?? ""); + $rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true; + + if (!$username || !$password) { + http_response_code(400); + echo json_encode(["error" => "Username and password are required"]); + exit(); + } + + if (!preg_match(REGEX_USER, $username)) { + http_response_code(400); + echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]); + exit(); + } + + $ip = $_SERVER['REMOTE_ADDR']; + $currentTime = time(); + $attemptsFile = USERS_DIR . 'failed_logins.json'; + $failedAttempts = AuthModel::loadFailedAttempts($attemptsFile); + $maxAttempts = 5; + $lockoutTime = 30 * 60; // 30 minutes + + if (isset($failedAttempts[$ip])) { + $attemptData = $failedAttempts[$ip]; + if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { + http_response_code(429); + echo json_encode(["error" => "Too many failed login attempts. Please try again later."]); + exit(); + } + } + + $user = AuthModel::authenticate($username, $password); + if ($user !== false) { + // Handle TOTP if required. + if (!empty($user['totp_secret'])) { + if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) { + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $user['totp_secret']; + echo json_encode([ + "totp_required" => true, + "message" => "TOTP code required" + ]); + exit(); + } else { + $tfa = new \RobThree\Auth\TwoFactorAuth( + new GoogleChartsQrCodeProvider(), + 'FileRise', + 6, + 30, + Algorithm::Sha1 + ); + $providedCode = trim($data['totp_code']); + if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) { + echo json_encode(["error" => "Invalid TOTP code"]); + exit(); + } + } + } + + // Clear failed attempts. + if (isset($failedAttempts[$ip])) { + unset($failedAttempts[$ip]); + AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts); + } + + session_regenerate_id(true); + $_SESSION["authenticated"] = true; + $_SESSION["username"] = $username; + $_SESSION["isAdmin"] = ($user['role'] === "1"); + $_SESSION["folderOnly"] = loadUserPermissions($username); + + // Handle "remember me" + if ($rememberMe) { + $persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; + $tokenPersistent = bin2hex(random_bytes(32)); + $expiry = time() + (30 * 24 * 60 * 60); + $persistentTokens = []; + if (file_exists($persistentTokensFile)) { + $encryptedContent = file_get_contents($persistentTokensFile); + $decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']); + $persistentTokens = json_decode($decryptedContent, true); + if (!is_array($persistentTokens)) { + $persistentTokens = []; + } + } + $persistentTokens[$tokenPersistent] = [ + "username" => $username, + "expiry" => $expiry, + "isAdmin" => ($_SESSION["isAdmin"] === true) + ]; + $encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']); + file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX); + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + setcookie('remember_me_token', $tokenPersistent, $expiry, '/', '', $secure, true); + } + + echo json_encode([ + "status" => "ok", + "success" => "Login successful", + "isAdmin" => $_SESSION["isAdmin"], + "folderOnly" => $_SESSION["folderOnly"], + "username" => $_SESSION["username"] + ]); + } else { + // Record failed login attempt. + if (isset($failedAttempts[$ip])) { + $failedAttempts[$ip]['count']++; + $failedAttempts[$ip]['last_attempt'] = $currentTime; + } else { + $failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime]; + } + AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts); + $failedLogFile = USERS_DIR . 'failed_login.log'; + $logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL; + file_put_contents($failedLogFile, $logLine, FILE_APPEND); + http_response_code(401); + echo json_encode(["error" => "Invalid credentials"]); + } + } + + /** + * @OA\Get( + * path="/api/auth/checkAuth.php", + * summary="Check authentication status", + * description="Checks if the current session is authenticated. If the users file is missing or empty, returns a setup flag. Also returns information about admin privileges, TOTP status, and folder-only access.", + * operationId="checkAuth", + * tags={"Auth"}, + * @OA\Response( + * response=200, + * description="Returns authentication status and user details", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="authenticated", type="boolean", example=true), + * @OA\Property(property="isAdmin", type="boolean", example=true), + * @OA\Property(property="totp_enabled", type="boolean", example=false), + * @OA\Property(property="username", type="string", example="johndoe"), + * @OA\Property(property="folderOnly", type="boolean", example=false) + * ) + * ), + * @OA\Response( + * response=200, + * description="Setup mode (if the users file is missing or empty)", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="setup", type="boolean", example=true) + * ) + * ) + * ) + * + * Checks whether the user is authenticated or if the system is in setup mode. + * + * @return void Outputs a JSON response with authentication details. + */ + public function checkAuth(): void { + header('Content-Type: application/json'); + + $usersFile = USERS_DIR . USERS_FILE; + // If the users file does not exist or is empty, signal setup mode. + if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') { + error_log("checkAuth: users file not found or empty; entering setup mode."); + echo json_encode(["setup" => true]); + exit; + } + + // If the session is not authenticated, output false. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + echo json_encode(["authenticated" => false]); + exit; + } + + // Retrieve the username from the session. + $username = $_SESSION['username'] ?? ''; + // Determine TOTP enabled by checking the users file. + $totp_enabled = false; + if ($username) { + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if ($parts[0] === $username && isset($parts[3]) && trim($parts[3]) !== "") { + $totp_enabled = true; + break; + } + } + } + + // Determine admin status using AuthModel::getUserRole() + $userRole = AuthModel::getUserRole($username); + $isAdmin = ((int)$userRole === 1); + + $response = [ + "authenticated" => true, + "isAdmin" => $isAdmin, + "totp_enabled" => $totp_enabled, + "username" => $username, + "folderOnly" => $_SESSION["folderOnly"] ?? false + ]; + echo json_encode($response); + exit; + } + + /** + * @OA\Get( + * path="/api/auth/token.php", + * summary="Retrieve CSRF token and share URL", + * description="Returns the current CSRF token along with the configured share URL.", + * operationId="getToken", + * tags={"Auth"}, + * @OA\Response( + * response=200, + * description="CSRF token and share URL", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="csrf_token", type="string", example="0123456789abcdef..."), + * @OA\Property(property="share_url", type="string", example="https://yourdomain.com/share.php") + * ) + * ) + * ) + * + * Returns the CSRF token and share URL. + * + * @return void Outputs the JSON response. + */ + public function getToken(): void { + header('Content-Type: application/json'); + echo json_encode([ + "csrf_token" => $_SESSION['csrf_token'], + "share_url" => SHARE_URL + ]); + exit; + } + + /** + * @OA\Get( + * path="/api/auth/login_basic.php", + * summary="Authenticate using HTTP Basic Authentication", + * description="Performs HTTP Basic authentication. If credentials are missing, sends a 401 response prompting for Basic auth. On valid credentials, optionally handles TOTP verification and finalizes session login.", + * operationId="loginBasic", + * tags={"Auth"}, + * @OA\Response( + * response=200, + * description="Login successful; redirects to index.html", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="success", type="string", example="Login successful") + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized due to missing credentials or invalid credentials." + * ) + * ) + * + * Handles HTTP Basic authentication (with optional TOTP) and logs the user in. + * + * @return void Redirects on success or sends a 401 header. + */ + public function loginBasic(): void { + // Set header for plain-text or JSON as needed. + header('Content-Type: application/json'); + + // Check for HTTP Basic auth credentials. + if (!isset($_SERVER['PHP_AUTH_USER'])) { + header('WWW-Authenticate: Basic realm="FileRise Login"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'Authorization Required'; + exit; + } + + $username = trim($_SERVER['PHP_AUTH_USER']); + $password = trim($_SERVER['PHP_AUTH_PW']); + + // Validate username format. + if (!preg_match(REGEX_USER, $username)) { + header('WWW-Authenticate: Basic realm="FileRise Login"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'Invalid username format'; + exit; + } + + // Attempt authentication. + $role = AuthModel::authenticate($username, $password); + if ($role !== false) { + // Check for TOTP secret. + $secret = AuthModel::getUserTOTPSecret($username); + if ($secret) { + // If TOTP is required, store pending values and redirect to prompt for TOTP. + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $secret; + header("Location: index.html?totp_required=1"); + exit; + } + // Finalize login. + session_regenerate_id(true); + $_SESSION["authenticated"] = true; + $_SESSION["username"] = $username; + $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); + $_SESSION["folderOnly"] = AuthModel::loadFolderPermission($username); + + header("Location: index.html"); + exit; + } + // Invalid credentials; prompt again. + header('WWW-Authenticate: Basic realm="FileRise Login"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'Invalid credentials'; + exit; + } + + /** + * @OA\Post( + * path="/api/auth/logout.php", + * summary="Logout user", + * description="Clears the session, removes persistent login tokens, and redirects the user to the login page.", + * operationId="logoutUser", + * tags={"Auth"}, + * @OA\Response( + * response=302, + * description="Redirects to the login page with a logout flag." + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ) + * ) + * + * Logs the user out by clearing session data, removing persistent tokens, and destroying the session. + * + * @return void Redirects to index.html with a logout flag. + */ + public function logout(): void { + // Retrieve headers and check CSRF token. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + + // Log mismatch but do not prevent logout. + if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) { + error_log("CSRF token mismatch on logout. Proceeding with logout."); + } + + // Remove the "remember_me_token" from persistent tokens. + if (isset($_COOKIE['remember_me_token'])) { + $token = $_COOKIE['remember_me_token']; + $persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; + if (file_exists($persistentTokensFile)) { + $encryptedContent = file_get_contents($persistentTokensFile); + $decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']); + $persistentTokens = json_decode($decryptedContent, true); + if (is_array($persistentTokens) && isset($persistentTokens[$token])) { + unset($persistentTokens[$token]); + $newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']); + file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX); + } + } + // Clear the cookie. + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true); + } + + // Clear session data. + $_SESSION = []; + + // Clear the session cookie. + if (ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, + $params["path"], $params["domain"], + $params["secure"], $params["httponly"] + ); + } + + // Destroy the session. + session_destroy(); + + // Redirect the user to the login page (or index) with a logout flag. + header("Location: index.html?logout=1"); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/fileController.php b/src/controllers/fileController.php new file mode 100644 index 0000000..d278384 --- /dev/null +++ b/src/controllers/fileController.php @@ -0,0 +1,1513 @@ + "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Check user permissions (assuming loadUserPermissions() is available). + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if (!empty($userPermissions['readOnly'])) { + echo json_encode(["error" => "Read-only users are not allowed to copy files."]); + exit; + } + + // Get JSON input data. + $data = json_decode(file_get_contents("php://input"), true); + if ( + !$data || + !isset($data['source']) || + !isset($data['destination']) || + !isset($data['files']) + ) { + http_response_code(400); + echo json_encode(["error" => "Invalid request"]); + exit; + } + + $sourceFolder = trim($data['source']); + $destinationFolder = trim($data['destination']); + $files = $data['files']; + + // Validate folder names. + if ($sourceFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $sourceFolder)) { + echo json_encode(["error" => "Invalid source folder name."]); + exit; + } + if ($destinationFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $destinationFolder)) { + echo json_encode(["error" => "Invalid destination folder name."]); + exit; + } + + // Delegate to the model. + $result = FileModel::copyFiles($sourceFolder, $destinationFolder, $files); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/file/deleteFiles.php", + * summary="Delete files (move to trash)", + * description="Moves the specified files from the given folder to the trash and updates metadata accordingly.", + * operationId="deleteFiles", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"files"}, + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property( + * property="files", + * type="array", + * @OA\Items(type="string", example="example.pdf") + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="Files moved to Trash successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="Files moved to Trash: file1.pdf, file2.doc") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or permission denied" + * ) + * ) + * + * Handles deletion of files (moves them to Trash) by updating metadata. + * + * @return void Outputs JSON response. + */ + public function deleteFiles() { + header('Content-Type: application/json'); + + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Load user's permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to delete files."]); + exit; + } + + // Get JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if (!isset($data['files']) || !is_array($data['files'])) { + http_response_code(400); + echo json_encode(["error" => "No file names provided"]); + exit; + } + + // Determine folder; default to 'root'. + $folder = isset($data['folder']) ? trim($data['folder']) : 'root'; + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + $folder = trim($folder, "/\\ "); + + // Delegate to the FileModel. + $result = FileModel::deleteFiles($folder, $data['files']); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/file/moveFiles.php", + * summary="Move files between folders", + * description="Moves files from a source folder to a destination folder, updating metadata accordingly.", + * operationId="moveFiles", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"source", "destination", "files"}, + * @OA\Property(property="source", type="string", example="root"), + * @OA\Property(property="destination", type="string", example="Archives"), + * @OA\Property( + * property="files", + * type="array", + * @OA\Items(type="string", example="report.pdf") + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="Files moved successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="Files moved successfully") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request or input" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or permission denied" + * ) + * ) + * + * Handles moving files from a source folder to a destination folder. + * + * @return void Outputs JSON response. + */ + public function moveFiles() { + header('Content-Type: application/json'); + + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Verify that the user is not read-only. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if (!empty($userPermissions['readOnly'])) { + echo json_encode(["error" => "Read-only users are not allowed to move files."]); + exit; + } + + // Get JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if ( + !$data || + !isset($data['source']) || + !isset($data['destination']) || + !isset($data['files']) + ) { + http_response_code(400); + echo json_encode(["error" => "Invalid request"]); + exit; + } + + $sourceFolder = trim($data['source']) ?: 'root'; + $destinationFolder = trim($data['destination']) ?: 'root'; + + // Validate folder names. + if ($sourceFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $sourceFolder)) { + echo json_encode(["error" => "Invalid source folder name."]); + exit; + } + if ($destinationFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $destinationFolder)) { + echo json_encode(["error" => "Invalid destination folder name."]); + exit; + } + + // Delegate to the model. + $result = FileModel::moveFiles($sourceFolder, $destinationFolder, $data['files']); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/file/renameFile.php", + * summary="Rename a file", + * description="Renames a file within a specified folder and updates folder metadata. If a file with the new name exists, a unique name is generated.", + * operationId="renameFile", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder", "oldName", "newName"}, + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property(property="oldName", type="string", example="oldfile.pdf"), + * @OA\Property(property="newName", type="string", example="newfile.pdf") + * ) + * ), + * @OA\Response( + * response=200, + * description="File renamed successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="File renamed successfully"), + * @OA\Property(property="newName", type="string", example="newfile.pdf") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid input" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or permission denied" + * ) + * ) + * + * Handles renaming a file by validating input and updating folder metadata. + * + * @return void Outputs a JSON response. + */ + public function renameFile() { + header('Content-Type: application/json'); + header("Cache-Control: no-cache, no-store, must-revalidate"); + header("Pragma: no-cache"); + header("Expires: 0"); + + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Verify user permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to rename files."]); + exit; + } + + // Get JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) { + http_response_code(400); + echo json_encode(["error" => "Invalid input"]); + exit; + } + + $folder = trim($data['folder']) ?: 'root'; + // Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes. + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + echo json_encode(["error" => "Invalid folder name"]); + exit; + } + + $oldName = basename(trim($data['oldName'])); + $newName = basename(trim($data['newName'])); + + // Validate file names. + if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) { + echo json_encode(["error" => "Invalid file name."]); + exit; + } + + // Delegate the renaming operation to the model. + $result = FileModel::renameFile($folder, $oldName, $newName); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/file/saveFile.php", + * summary="Save a file", + * description="Saves file content to disk in a specified folder and updates metadata accordingly.", + * operationId="saveFile", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"fileName", "content"}, + * @OA\Property(property="fileName", type="string", example="document.txt"), + * @OA\Property(property="content", type="string", example="File content here"), + * @OA\Property(property="folder", type="string", example="Documents") + * ) + * ), + * @OA\Response( + * response=200, + * description="File saved successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="File saved successfully") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request data" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or read-only permission" + * ) + * ) + * + * Handles saving a file's content and updating the corresponding metadata. + * + * @return void Outputs a JSON response. + */ + public function saveFile() { + header('Content-Type: application/json'); + + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Check if the user is allowed to save files (not read-only). + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to save files."]); + exit; + } + + // Get JSON input. + $data = json_decode(file_get_contents("php://input"), true); + + if (!$data) { + echo json_encode(["error" => "No data received"]); + exit; + } + + if (!isset($data["fileName"]) || !isset($data["content"])) { + echo json_encode(["error" => "Invalid request data", "received" => $data]); + exit; + } + + $fileName = basename($data["fileName"]); + // Determine the folder. Default to "root" if not provided. + $folder = isset($data["folder"]) ? trim($data["folder"]) : "root"; + + // Validate folder if not root. + if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) { + echo json_encode(["error" => "Invalid folder name"]); + exit; + } + + $folder = trim($folder, "/\\ "); + + // Delegate to the model. + $result = FileModel::saveFile($folder, $fileName, $data["content"]); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/file/download.php", + * summary="Download a file", + * description="Downloads a file from a specified folder. The file is served inline for images or as an attachment for other types.", + * operationId="downloadFile", + * tags={"Files"}, + * @OA\Parameter( + * name="file", + * in="query", + * description="The name of the file to download", + * required=true, + * @OA\Schema(type="string", example="example.pdf") + * ), + * @OA\Parameter( + * name="folder", + * in="query", + * description="The folder in which the file is located. Defaults to root.", + * required=false, + * @OA\Schema(type="string", example="Documents") + * ), + * @OA\Response( + * response=200, + * description="File downloaded successfully" + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Access forbidden" + * ), + * @OA\Response( + * response=404, + * description="File not found" + * ), + * @OA\Response( + * response=500, + * description="Server error" + * ) + * ) + * + * Downloads a file by validating parameters and serving its content. + * + * @return void Outputs file content with appropriate headers. + */ + public function downloadFile() { + // Check if the user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Get GET parameters. + $file = isset($_GET['file']) ? basename($_GET['file']) : ''; + $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; + + // Validate the file name using REGEX_FILE_NAME. + if (!preg_match(REGEX_FILE_NAME, $file)) { + http_response_code(400); + echo json_encode(["error" => "Invalid file name."]); + exit; + } + + // Retrieve download info from the model. + $downloadInfo = FileModel::getDownloadInfo($folder, $file); + if (isset($downloadInfo['error'])) { + http_response_code( (in_array($downloadInfo['error'], ["File not found.", "Access forbidden."])) ? 404 : 400 ); + echo json_encode(["error" => $downloadInfo['error']]); + exit; + } + + // Serve the file. + $realFilePath = $downloadInfo['filePath']; + $mimeType = $downloadInfo['mimeType']; + header("Content-Type: " . $mimeType); + + // For images, serve inline; for others, force download. + $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); + $inlineImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico']; + if (in_array($ext, $inlineImageTypes)) { + header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); + } else { + header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); + } + header('Content-Length: ' . filesize($realFilePath)); + readfile($realFilePath); + exit; + } + + /** + * @OA\Post( + * path="/api/file/downloadZip.php", + * summary="Download a ZIP archive of selected files", + * description="Creates a ZIP archive of the specified files in a folder and serves it for download.", + * operationId="downloadZip", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder", "files"}, + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property( + * property="files", + * type="array", + * @OA\Items(type="string", example="example.pdf") + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="ZIP archive created and served", + * @OA\MediaType( + * mediaType="application/zip" + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad request or invalid input" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ), + * @OA\Response( + * response=500, + * description="Server error" + * ) + * ) + * + * Downloads a ZIP archive of the specified files. + * + * @return void Outputs the ZIP file for download. + */ + public function downloadZip() { + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Read and decode JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "Invalid input."]); + exit; + } + + $folder = $data['folder']; + $files = $data['files']; + + // Validate folder: if not "root", split and validate each segment. + if ($folder !== "root") { + $parts = explode('/', $folder); + foreach ($parts as $part) { + if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + } + } + + // Create ZIP archive using FileModel. + $result = FileModel::createZipArchive($folder, $files); + if (isset($result['error'])) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => $result['error']]); + exit; + } + + $zipPath = $result['zipPath']; + if (!file_exists($zipPath)) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(["error" => "ZIP archive not found."]); + exit; + } + + // Send headers to force download. + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="files.zip"'); + header('Content-Length: ' . filesize($zipPath)); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Pragma: no-cache'); + + // Output the ZIP file. + readfile($zipPath); + unlink($zipPath); + exit; + } + + /** + * @OA\Post( + * path="/api/file/extractZip.php", + * summary="Extract ZIP files", + * description="Extracts ZIP archives from a specified folder and updates metadata. Returns a list of extracted files.", + * operationId="extractZip", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder", "files"}, + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property( + * property="files", + * type="array", + * @OA\Items(type="string", example="archive.zip") + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="ZIP files extracted successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="extractedFiles", type="array", @OA\Items(type="string")) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid input" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + * + * Handles the extraction of ZIP files from a given folder. + * + * @return void Outputs JSON response. + */ + public function extractZip() { + header('Content-Type: application/json'); + + // --- CSRF Protection --- + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Read and decode JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) { + http_response_code(400); + echo json_encode(["error" => "Invalid input."]); + exit; + } + + $folder = $data['folder']; + $files = $data['files']; + + // Validate folder name. + if ($folder !== "root") { + $parts = explode('/', trim($folder)); + foreach ($parts as $part) { + if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) { + http_response_code(400); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + } + } + + // Delegate to the model. + $result = FileModel::extractZipArchive($folder, $files); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/file/share.php", + * summary="Access a shared file", + * description="Serves a shared file based on a share token. If the file is password protected and no password is provided, a password entry form is returned.", + * operationId="shareFile", + * tags={"Files"}, + * @OA\Parameter( + * name="token", + * in="query", + * description="The share token", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="pass", + * in="query", + * description="The password for the share if required", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Response( + * response=200, + * description="File served or password form rendered", + * @OA\MediaType(mediaType="application/octet-stream") + * ), + * @OA\Response( + * response=400, + * description="Missing token or invalid request" + * ), + * @OA\Response( + * response=403, + * description="Link expired, invalid password, or forbidden access" + * ), + * @OA\Response( + * response=404, + * description="Share link or file not found" + * ) + * ) + * + * Shares a file based on a share token. If the share record is password-protected and no password is provided, + * an HTML form prompting for the password is returned. + * + * @return void Outputs either HTML (password form) or serves the file. + */ + public function shareFile() { + // Retrieve and sanitize GET parameters. + $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING); + $providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING); + + if (empty($token)) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "Missing token."]); + exit; + } + + // Get share record from the model. + $record = FileModel::getShareRecord($token); + if (!$record) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(["error" => "Share link not found."]); + exit; + } + + // Check expiration. + if (time() > $record['expires']) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(["error" => "This link has expired."]); + exit; + } + + // If a password is required and not provided, show an HTML form. + if (!empty($record['password']) && empty($providedPass)) { + header("Content-Type: text/html; charset=utf-8"); + ?> + + + + + + Enter Password + + + +

This file is protected by a password.

+
+ + + + +
+ + + "Invalid password."]); + exit; + } + } + + // Build file path securely. + $folder = trim($record['folder'], "/\\ "); + $file = $record['file']; + $filePath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; + if (!empty($folder) && strtolower($folder) !== 'root') { + $filePath .= $folder . DIRECTORY_SEPARATOR; + } + $filePath .= $file; + + $realFilePath = realpath($filePath); + $uploadDirReal = realpath(UPLOAD_DIR); + if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(["error" => "File not found."]); + exit; + } + if (!file_exists($realFilePath)) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(["error" => "File not found."]); + exit; + } + + // Serve the file. + $mimeType = mime_content_type($realFilePath); + header("Content-Type: " . $mimeType); + $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); + if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) { + header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); + } else { + header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); + } + header("Cache-Control: no-store, no-cache, must-revalidate"); + header("Pragma: no-cache"); + header('Content-Length: ' . filesize($realFilePath)); + + readfile($realFilePath); + exit; + } + + /** + * @OA\Post( + * path="/api/file/createShareLink.php", + * summary="Create a share link for a file", + * description="Generates a secure share link token for a specific file with an optional password protection and expiration time.", + * operationId="createShareLink", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder", "file"}, + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property(property="file", type="string", example="report.pdf"), + * @OA\Property(property="expirationMinutes", type="integer", example=60), + * @OA\Property(property="password", type="string", example="secret") + * ) + * ), + * @OA\Response( + * response=200, + * description="Share link created successfully", + * @OA\JsonContent( + * @OA\Property(property="token", type="string", example="a1b2c3d4e5f6..."), + * @OA\Property(property="expires", type="integer", example=1621234567) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request data" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Read-only users are not allowed to create share links" + * ) + * ) + * + * Creates a share link for a file. + * + * @return void Outputs JSON response. + */ + public function createShareLink() { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Check user permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + http_response_code(403); + echo json_encode(["error" => "Read-only users are not allowed to create share links."]); + exit; + } + + // Parse POST JSON input. + $input = json_decode(file_get_contents("php://input"), true); + if (!$input) { + http_response_code(400); + echo json_encode(["error" => "Invalid input."]); + exit; + } + + // Extract parameters. + $folder = isset($input['folder']) ? trim($input['folder']) : ""; + $file = isset($input['file']) ? basename($input['file']) : ""; + $expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60; + $password = isset($input['password']) ? $input['password'] : ""; + + // Validate folder. + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + http_response_code(400); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + + // Delegate share link creation to the model. + $result = FileModel::createShareLink($folder, $file, $expirationMinutes, $password); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/file/getTrashItems.php", + * summary="Get trash items", + * description="Retrieves a list of files that have been moved to Trash, enriched with metadata such as who deleted them and when.", + * operationId="getTrashItems", + * tags={"Files"}, + * @OA\Response( + * response=200, + * description="Trash items retrieved successfully", + * @OA\JsonContent(type="array", @OA\Items(type="object")) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ) + * ) + * + * Retrieves trash items from the trash metadata file. + * + * @return void Outputs JSON response with trash items. + */ + public function getTrashItems() { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Delegate to the model. + $trashItems = FileModel::getTrashItems(); + echo json_encode($trashItems); + } + + /** + * @OA\Post( + * path="/api/file/restoreFiles.php", + * summary="Restore trashed files", + * description="Restores files from Trash based on provided trash file identifiers and updates metadata.", + * operationId="restoreFiles", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"files"}, + * @OA\Property(property="files", type="array", @OA\Items(type="string", example="trashedFile_1623456789.zip")) + * ) + * ), + * @OA\Response( + * response=200, + * description="Files restored successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="Items restored: file1, file2"), + * @OA\Property(property="restored", type="array", @OA\Items(type="string")) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + * + * Restores files from Trash based on provided trash file names. + * + * @return void Outputs JSON response. + */ + public function restoreFiles() { + header('Content-Type: application/json'); + + // CSRF Protection. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Read POST input. + $data = json_decode(file_get_contents("php://input"), true); + if (!isset($data['files']) || !is_array($data['files'])) { + http_response_code(400); + echo json_encode(["error" => "No file or folder identifiers provided"]); + exit; + } + + // Delegate restoration to the model. + $result = FileModel::restoreFiles($data['files']); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/file/deleteTrashFiles.php", + * summary="Delete trash files", + * description="Deletes trash items based on provided trash file identifiers from the trash metadata and removes the files from disk.", + * operationId="deleteTrashFiles", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * oneOf={ + * @OA\Schema( + * required={"deleteAll"}, + * @OA\Property(property="deleteAll", type="boolean", example=true) + * ), + * @OA\Schema( + * required={"files"}, + * @OA\Property( + * property="files", + * type="array", + * @OA\Items(type="string", example="trashedfile_1234567890") + * ) + * ) + * } + * ) + * ), + * @OA\Response( + * response=200, + * description="Trash items deleted successfully", + * @OA\JsonContent( + * @OA\Property(property="deleted", type="array", @OA\Items(type="string")) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid input" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + * + * Deletes trash files by processing provided trash file identifiers. + * + * @return void Outputs a JSON response. + */ + public function deleteTrashFiles() { + header('Content-Type: application/json'); + + // CSRF Protection. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Read and decode JSON input. + $data = json_decode(file_get_contents("php://input"), true); + if (!$data) { + http_response_code(400); + echo json_encode(["error" => "Invalid input"]); + exit; + } + + // Determine deletion mode. + $filesToDelete = []; + if (isset($data['deleteAll']) && $data['deleteAll'] === true) { + // In this case, we need to delete all trash items. + // Load current trash metadata. + $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; + $shareFile = $trashDir . "trash.json"; + if (file_exists($shareFile)) { + $json = file_get_contents($shareFile); + $tempData = json_decode($json, true); + if (is_array($tempData)) { + foreach ($tempData as $item) { + if (isset($item['trashName'])) { + $filesToDelete[] = $item['trashName']; + } + } + } + } + } elseif (isset($data['files']) && is_array($data['files'])) { + $filesToDelete = $data['files']; + } else { + http_response_code(400); + echo json_encode(["error" => "No trash file identifiers provided"]); + exit; + } + + // Delegate deletion to the model. + $result = FileModel::deleteTrashFiles($filesToDelete); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/file/getFileTag.php", + * summary="Retrieve file tags", + * description="Retrieves tags from the createdTags.json metadata file.", + * operationId="getFileTags", + * tags={"Files"}, + * @OA\Response( + * response=200, + * description="File tags retrieved successfully", + * @OA\JsonContent( + * type="array", + * @OA\Items(type="object") + * ) + * ) + * ) + * + * Retrieves file tags from the createdTags.json metadata file. + * + * @return void Outputs JSON response with file tags. + */ + public function getFileTags(): void { + header('Content-Type: application/json; charset=utf-8'); + + $tags = FileModel::getFileTags(); + echo json_encode($tags); + exit; + } + + /** + * @OA\Post( + * path="/api/file/saveFileTag.php", + * summary="Save file tags", + * description="Saves tag data for a specified file and updates global tag data. For folder-specific tags, saves to the folder's metadata file.", + * operationId="saveFileTag", + * tags={"Files"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"file", "tags"}, + * @OA\Property(property="file", type="string", example="document.txt"), + * @OA\Property(property="folder", type="string", example="Documents"), + * @OA\Property( + * property="tags", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="name", type="string", example="Important"), + * @OA\Property(property="color", type="string", example="#FF0000") + * ) + * ), + * @OA\Property(property="deleteGlobal", type="boolean", example=false), + * @OA\Property(property="tagToDelete", type="string", example="OldTag") + * ) + * ), + * @OA\Response( + * response=200, + * description="Tag data saved successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="Tag data saved successfully."), + * @OA\Property(property="globalTags", type="array", @OA\Items(type="object")) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid request data" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or insufficient permissions" + * ) + * ) + * + * Saves tag data for a file and updates the global tag repository. + * + * @return void Outputs JSON response. + */ + public function saveFileTag(): void { + header("Cache-Control: no-cache, no-store, must-revalidate"); + header("Pragma: no-cache"); + header("Expires: 0"); + header('Content-Type: application/json'); + + // CSRF Protection. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = $headersArr['x-csrf-token'] ?? ''; + if (!isset($_SESSION['csrf_token']) || trim($csrfHeader) !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Check that the user is not read-only. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to file tags"]); + exit; + } + + // Retrieve and sanitize input. + $data = json_decode(file_get_contents('php://input'), true); + if (!$data) { + http_response_code(400); + echo json_encode(["error" => "No data received"]); + exit; + } + + $file = isset($data['file']) ? trim($data['file']) : ''; + $folder = isset($data['folder']) ? trim($data['folder']) : 'root'; + $tags = $data['tags'] ?? []; + $deleteGlobal = isset($data['deleteGlobal']) ? (bool)$data['deleteGlobal'] : false; + $tagToDelete = isset($data['tagToDelete']) ? trim($data['tagToDelete']) : null; + + if ($file === '') { + http_response_code(400); + echo json_encode(["error" => "No file specified."]); + exit; + } + + // Validate folder name. + if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + http_response_code(400); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + + // Delegate to the model. + $result = FileModel::saveFileTag($folder, $file, $tags, $deleteGlobal, $tagToDelete); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/file/getFileList.php", + * summary="Get file list", + * description="Retrieves a list of files from a specified folder along with global tags and metadata.", + * operationId="getFileList", + * tags={"Files"}, + * @OA\Parameter( + * name="folder", + * in="query", + * description="Folder name (defaults to 'root')", + * required=false, + * @OA\Schema(type="string", example="Documents") + * ), + * @OA\Response( + * response=200, + * description="File list retrieved successfully", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="files", type="array", @OA\Items(type="object")), + * @OA\Property(property="globalTags", type="array", @OA\Items(type="object")) + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ) + * ) + * + * Retrieves the file list and associated metadata for the specified folder. + * + * @return void Outputs JSON response. + */ + public function getFileList(): void { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Retrieve the folder from GET; default to "root". + $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + http_response_code(400); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + + // Delegate to the model. + $result = FileModel::getFileList($folder); + if (isset($result['error'])) { + http_response_code(400); + } + echo json_encode($result); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/folderController.php b/src/controllers/folderController.php new file mode 100644 index 0000000..0f7d2bb --- /dev/null +++ b/src/controllers/folderController.php @@ -0,0 +1,907 @@ + "Unauthorized"]); + exit; + } + + // Ensure the request method is POST. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['error' => 'Invalid request method.']); + exit; + } + + // CSRF check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = $headersArr['x-csrf-token'] ?? ''; + if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(['error' => 'Invalid CSRF token.']); + exit; + } + + // Check permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to create folders."]); + exit; + } + + // Get and decode JSON input. + $input = json_decode(file_get_contents('php://input'), true); + if (!isset($input['folderName'])) { + echo json_encode(['error' => 'Folder name not provided.']); + exit; + } + + $folderName = trim($input['folderName']); + $parent = isset($input['parent']) ? trim($input['parent']) : ""; + + // Basic sanitation for folderName. + if (!preg_match(REGEX_FOLDER_NAME, $folderName)) { + echo json_encode(['error' => 'Invalid folder name.']); + exit; + } + + // Optionally sanitize the parent. + if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) { + echo json_encode(['error' => 'Invalid parent folder name.']); + exit; + } + + // Delegate to FolderModel. + $result = FolderModel::createFolder($folderName, $parent); + echo json_encode($result); + exit; + } + + /** + * @OA\Post( + * path="/api/folder/deleteFolder.php", + * summary="Delete an empty folder", + * description="Deletes a specified folder if it is empty and not the root folder, and also removes its metadata file.", + * operationId="deleteFolder", + * tags={"Folders"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder"}, + * @OA\Property(property="folder", type="string", example="Documents/Subfolder") + * ) + * ), + * @OA\Response( + * response=200, + * description="Folder deleted successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="boolean", example=true) + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request (e.g., invalid folder name or folder not empty)" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or permission denied" + * ) + * ) + * + * Deletes a folder if it is empty and not the root folder. + * + * @return void Outputs a JSON response. + */ + public function deleteFolder(): void { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Ensure the request is a POST. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(["error" => "Invalid request method."]); + exit; + } + + // CSRF Protection. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token."]); + exit; + } + + // Check user permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to delete folders."]); + exit; + } + + // Get and decode JSON input. + $input = json_decode(file_get_contents('php://input'), true); + if (!isset($input['folder'])) { + echo json_encode(["error" => "Folder name not provided."]); + exit; + } + + $folder = trim($input['folder']); + // Prevent deletion of the root folder. + if (strtolower($folder) === 'root') { + echo json_encode(["error" => "Cannot delete root folder."]); + exit; + } + + // Delegate to the model. + $result = FolderModel::deleteFolder($folder); + echo json_encode($result); + exit; + } + + /** + * @OA\Post( + * path="/api/folder/renameFolder.php", + * summary="Rename a folder", + * description="Renames an existing folder and updates its associated metadata files.", + * operationId="renameFolder", + * tags={"Folders"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"oldFolder", "newFolder"}, + * @OA\Property(property="oldFolder", type="string", example="Documents/OldFolder"), + * @OA\Property(property="newFolder", type="string", example="Documents/NewFolder") + * ) + * ), + * @OA\Response( + * response=200, + * description="Folder renamed successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="boolean", example=true) + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid folder names or folder does not exist" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or permission denied" + * ) + * ) + * + * Renames a folder by validating inputs and delegating to the model. + * + * @return void Outputs a JSON response. + */ + public function renameFolder(): void { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Ensure the request method is POST. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['error' => 'Invalid request method.']); + exit; + } + + // CSRF Protection. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token."]); + exit; + } + + // Check that the user is not read-only. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + echo json_encode(["error" => "Read-only users are not allowed to rename folders."]); + exit; + } + + // Get JSON input. + $input = json_decode(file_get_contents('php://input'), true); + if (!isset($input['oldFolder']) || !isset($input['newFolder'])) { + echo json_encode(['error' => 'Required folder names not provided.']); + exit; + } + + $oldFolder = trim($input['oldFolder']); + $newFolder = trim($input['newFolder']); + + // Validate folder names. + if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) { + echo json_encode(['error' => 'Invalid folder name(s).']); + exit; + } + + // Delegate to the model. + $result = FolderModel::renameFolder($oldFolder, $newFolder); + echo json_encode($result); + exit; + } + + /** + * @OA\Get( + * path="/api/folder/getFolderList.php", + * summary="Get list of folders", + * description="Retrieves the list of folders in the upload directory, including file counts and metadata file names for each folder.", + * operationId="getFolderList", + * tags={"Folders"}, + * @OA\Parameter( + * name="folder", + * in="query", + * description="Optional folder name to filter the listing", + * required=false, + * @OA\Schema(type="string", example="Documents") + * ), + * @OA\Response( + * response=200, + * description="Folder list retrieved successfully", + * @OA\JsonContent( + * type="array", + * @OA\Items(type="object") + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=400, + * description="Bad request" + * ) + * ) + * + * Retrieves the folder list and associated metadata. + * + * @return void Outputs JSON response. + */ + public function getFolderList(): void { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Optionally, you might add further input validation if necessary. + $folderList = FolderModel::getFolderList(); + echo json_encode($folderList); + exit; + } + + /** + * @OA\Get( + * path="/api/folder/shareFolder.php", + * summary="Display a shared folder", + * description="Renders an HTML view of a shared folder's contents. Supports password protection, file listing with pagination, and an upload container if uploads are allowed.", + * operationId="shareFolder", + * tags={"Folders"}, + * @OA\Parameter( + * name="token", + * in="query", + * description="The share token for the folder", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="pass", + * in="query", + * description="The password if the folder is protected", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="page", + * in="query", + * description="Page number for pagination", + * required=false, + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Shared folder displayed", + * @OA\MediaType(mediaType="text/html") + * ), + * @OA\Response( + * response=400, + * description="Invalid request" + * ), + * @OA\Response( + * response=403, + * description="Access forbidden (expired link or invalid password)" + * ), + * @OA\Response( + * response=404, + * description="Share folder not found" + * ) + * ) + * + * Displays a shared folder with file listings, pagination, and an upload container if allowed. + * + * @return void Outputs HTML content. + */ + public function shareFolder(): void { + // Retrieve GET parameters. + $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; + } + + // Delegate to the model. + $data = FolderModel::getSharedFolderData($token, $providedPass, $page); + + // If a password is needed, output an HTML form. + if (isset($data['needs_password']) && $data['needs_password'] === true) { + header("Content-Type: text/html; charset=utf-8"); + ?> + + + + + + Enter Password + + + +
+

Folder Protected

+

This folder is protected by a password. Please enter the password to view its contents.

+
+ + + + +
+
+ + + $data['error']]); + exit; + } + + // Extract data for the HTML view. + $folderName = $data['folder']; + $files = $data['files']; + $currentPage = $data['currentPage']; + $totalPages = $data['totalPages']; + + function formatBytes($bytes) { + if ($bytes < 1024) { + return $bytes . " B"; + } elseif ($bytes < 1024 * 1024) { + return round($bytes / 1024, 2) . " KB"; + } elseif ($bytes < 1024 * 1024 * 1024) { + return round($bytes / (1024 * 1024), 2) . " MB"; + } else { + return round($bytes / (1024 * 1024 * 1024), 2) . " GB"; + } + } + + // Build the HTML view. + header("Content-Type: text/html; charset=utf-8"); + ?> + + + + + Shared Folder: <?php echo htmlspecialchars($folderName, ENT_QUOTES, 'UTF-8'); ?> + + + + +
+

Shared Folder:

+
+
+ + + + +
+ +

This folder is empty.

+ + + + + + + + + + + + + + + + +
FilenameSize
+ + + + +
+ +
+ + + + + + + + + +
+

Upload File (50mb max size)

+
+ + + +

+ +
+
+ +
+ + + + + + "Unauthorized"]); + exit; + } + + // Check that the user is not read-only. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) { + http_response_code(403); + echo json_encode(["error" => "Read-only users are not allowed to create share folders."]); + exit; + } + + // Retrieve and decode POST input. + $input = json_decode(file_get_contents("php://input"), true); + if (!$input || !isset($input['folder'])) { + http_response_code(400); + echo json_encode(["error" => "Invalid input."]); + exit; + } + + $folder = trim($input['folder']); + $expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60; + $password = isset($input['password']) ? $input['password'] : ""; + $allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0; + + // Delegate to the model. + $result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload); + echo json_encode($result); + exit; + } + + /** + * @OA\Get( + * path="/api/folder/downloadSharedFile.php", + * summary="Download a file from a shared folder", + * description="Retrieves and serves a file from a shared folder based on a share token.", + * operationId="downloadSharedFile", + * tags={"Folders"}, + * @OA\Parameter( + * name="token", + * in="query", + * description="The share folder token", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="file", + * in="query", + * description="The filename to download", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Response( + * response=200, + * description="File served successfully", + * @OA\MediaType(mediaType="application/octet-stream") + * ), + * @OA\Response( + * response=400, + * description="Bad Request (missing parameters, invalid file name, etc.)" + * ), + * @OA\Response( + * response=403, + * description="Access forbidden (e.g., expired share link)" + * ), + * @OA\Response( + * response=404, + * description="File not found" + * ) + * ) + * + * Downloads a file from a shared folder based on a token. + * + * @return void Outputs the file with proper headers. + */ + public function downloadSharedFile(): void { + // Retrieve and sanitize GET parameters. + $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING); + $file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING); + + if (empty($token) || empty($file)) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "Missing token or file parameter."]); + exit; + } + + // Delegate to the model. + $result = FolderModel::getSharedFileInfo($token, $file); + if (isset($result['error'])) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(["error" => $result['error']]); + exit; + } + + $realFilePath = $result['realFilePath']; + $mimeType = $result['mimeType']; + + // Serve the file. + header("Content-Type: " . $mimeType); + $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)); + if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) { + header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"'); + } else { + header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"'); + } + header('Content-Length: ' . filesize($realFilePath)); + readfile($realFilePath); + exit; + } + + /** + * @OA\Post( + * path="/api/folder/uploadToSharedFolder.php", + * summary="Upload a file to a shared folder", + * description="Handles file upload to a shared folder using a share token. Validates file size, extension, and uploads the file to the shared folder, updating metadata accordingly.", + * operationId="uploadToSharedFolder", + * tags={"Folders"}, + * @OA\RequestBody( + * required=true, + * description="Multipart form data containing the share token and file to upload.", + * @OA\MediaType( + * mediaType="multipart/form-data", + * @OA\Schema( + * required={"token", "fileToUpload"}, + * @OA\Property(property="token", type="string"), + * @OA\Property(property="fileToUpload", type="string", format="binary") + * ) + * ) + * ), + * @OA\Response( + * response=302, + * description="Redirects to the shared folder page on success." + * ), + * @OA\Response( + * response=400, + * description="Bad Request (missing token, file upload error, file type/size not allowed)" + * ), + * @OA\Response( + * response=403, + * description="Forbidden (share link expired or uploads not allowed)" + * ), + * @OA\Response( + * response=500, + * description="Server error during file move" + * ) + * ) + * + * Handles uploading a file to a shared folder. + * + * @return void Redirects upon successful upload or outputs JSON errors. + */ + public function uploadToSharedFolder(): void { + // Ensure request is POST. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + header('Content-Type: application/json'); + echo json_encode(["error" => "Method not allowed."]); + exit; + } + + // Ensure the share token is provided. + if (empty($_POST['token'])) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "Missing share token."]); + exit; + } + $token = trim($_POST['token']); + + // Delegate the upload to the model. + if (!isset($_FILES['fileToUpload'])) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(["error" => "No file was uploaded."]); + exit; + } + $fileUpload = $_FILES['fileToUpload']; + + $result = FolderModel::uploadToSharedFolder($token, $fileUpload); + if (isset($result['error'])) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode($result); + exit; + } + + // Optionally, set a flash message in session. + $_SESSION['upload_message'] = "File uploaded successfully."; + + // Redirect back to the shared folder view. + $redirectUrl = "api/folder/shareFolder.php?token=" . urlencode($token); + header("Location: " . $redirectUrl); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/uploadController.php b/src/controllers/uploadController.php new file mode 100644 index 0000000..eea9b65 --- /dev/null +++ b/src/controllers/uploadController.php @@ -0,0 +1,177 @@ + "Invalid CSRF token"]); + exit; + } + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + // Check user permissions. + $username = $_SESSION['username'] ?? ''; + $userPermissions = loadUserPermissions($username); + if ($username && !empty($userPermissions['disableUpload'])) { + http_response_code(403); + echo json_encode(["error" => "Upload disabled for this user."]); + exit; + } + + // Delegate to the model. + $result = UploadModel::handleUpload($_POST, $_FILES); + + // For chunked uploads, output JSON (e.g., "chunk uploaded" status). + if (isset($result['error'])) { + http_response_code(400); + echo json_encode($result); + exit; + } + if (isset($result['status'])) { + echo json_encode($result); + exit; + } + + // Otherwise, for full upload success, set a flash message and redirect. + $_SESSION['upload_message'] = "File uploaded successfully."; + exit; + } + + /** + * @OA\Post( + * path="/api/upload/removeChunks.php", + * summary="Remove chunked upload temporary directory", + * description="Removes the temporary directory used for chunked uploads, given a folder name matching the expected resumable pattern.", + * operationId="removeChunks", + * tags={"Uploads"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"folder"}, + * @OA\Property(property="folder", type="string", example="resumable_myupload123") + * ) + * ), + * @OA\Response( + * response=200, + * description="Temporary folder removed successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="Temporary folder removed.") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid input (e.g., missing folder or invalid folder name)" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + * + * Removes the temporary upload folder for chunked uploads. + * + * @return void Outputs a JSON response. + */ + public function removeChunks(): void { + header('Content-Type: application/json'); + + // CSRF Protection: Validate token from POST data. + $receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : ''; + if ($receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Check that the folder parameter is provided. + if (!isset($_POST['folder'])) { + http_response_code(400); + echo json_encode(["error" => "No folder specified"]); + exit; + } + + $folder = $_POST['folder']; + $result = UploadModel::removeChunks($folder); + echo json_encode($result); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/userController.php b/src/controllers/userController.php new file mode 100644 index 0000000..6377f90 --- /dev/null +++ b/src/controllers/userController.php @@ -0,0 +1,950 @@ + "Unauthorized"]); + exit; + } + + // Retrieve users using the model + $users = userModel::getAllUsers(); + echo json_encode($users); + } + + /** + * @OA\Post( + * path="/api/addUser.php", + * summary="Add a new user", + * description="Adds a new user to the system. In setup mode, the new user is automatically made admin.", + * operationId="addUser", + * tags={"Users"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"username", "password"}, + * @OA\Property(property="username", type="string", example="johndoe"), + * @OA\Property(property="password", type="string", example="securepassword"), + * @OA\Property(property="isAdmin", type="boolean", example=true) + * ) + * ), + * @OA\Response( + * response=200, + * description="User added successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="User added successfully") + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ) + * ) + */ + + public function addUser() + { + header('Content-Type: application/json'); + + $usersFile = USERS_DIR . USERS_FILE; + + // Determine if we're in setup mode. + // Setup mode means the "setup" query parameter is passed + // and users.txt is missing, empty, or contains only whitespace. + $isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1'); + if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) { + // Allow initial admin creation without session or CSRF checks. + $setupMode = true; + } else { + $setupMode = false; + // In non-setup mode, perform CSRF token and authentication checks. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + if ( + !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || + !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true + ) { + echo json_encode(["error" => "Unauthorized"]); + exit; + } + } + + // Get the JSON input data. + $data = json_decode(file_get_contents("php://input"), true); + $newUsername = trim($data["username"] ?? ""); + $newPassword = trim($data["password"] ?? ""); + + // In setup mode, force the new user to be an admin. + if ($setupMode) { + $isAdmin = "1"; + } else { + $isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; + } + + // Validate that a username and password are provided. + if (!$newUsername || !$newPassword) { + echo json_encode(["error" => "Username and password required"]); + exit; + } + + // Validate username format. + if (!preg_match(REGEX_USER, $newUsername)) { + echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]); + exit; + } + + // Delegate the business logic to the model. + $result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode); + echo json_encode($result); + } + + /** + * @OA\Delete( + * path="/api/removeUser.php", + * summary="Remove a user", + * description="Removes the specified user from the system. Cannot remove the currently logged-in user.", + * operationId="removeUser", + * tags={"Users"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"username"}, + * @OA\Property(property="username", type="string", example="johndoe") + * ) + * ), + * @OA\Response( + * response=200, + * description="User removed successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="User removed successfully") + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + */ + + public function removeUser() + { + header('Content-Type: application/json'); + + // CSRF token check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Authentication and admin check. + if ( + !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || + !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true + ) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Retrieve JSON data. + $data = json_decode(file_get_contents("php://input"), true); + $usernameToRemove = trim($data["username"] ?? ""); + + if (!$usernameToRemove) { + echo json_encode(["error" => "Username is required"]); + exit; + } + + // Validate the username format. + if (!preg_match(REGEX_USER, $usernameToRemove)) { + echo json_encode(["error" => "Invalid username format"]); + exit; + } + + // Prevent removal of the currently logged-in user. + if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) { + echo json_encode(["error" => "Cannot remove yourself"]); + exit; + } + + // Delegate the removal logic to the model. + $result = userModel::removeUser($usernameToRemove); + echo json_encode($result); + } + + /** + * @OA\Get( + * path="/api/getUserPermissions.php", + * summary="Retrieve user permissions", + * description="Returns the permissions for the current user, or all permissions if the user is an admin.", + * operationId="getUserPermissions", + * tags={"Users"}, + * @OA\Response( + * response=200, + * description="Successful response with user permissions", + * @OA\JsonContent(type="object") + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ) + * ) + */ + + public function getUserPermissions() + { + header('Content-Type: application/json'); + + // Check if the user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Delegate to the model. + $permissions = userModel::getUserPermissions(); + echo json_encode($permissions); + } + + /** + * @OA\Put( + * path="/api/updateUserPermissions.php", + * summary="Update user permissions", + * description="Updates permissions for users. Only available to authenticated admin users.", + * operationId="updateUserPermissions", + * tags={"Users"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"permissions"}, + * @OA\Property( + * property="permissions", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="username", type="string", example="johndoe"), + * @OA\Property(property="folderOnly", type="boolean", example=true), + * @OA\Property(property="readOnly", type="boolean", example=false), + * @OA\Property(property="disableUpload", type="boolean", example=false) + * ) + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="User permissions updated successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="User permissions updated successfully.") + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ) + * ) + */ + + public function updateUserPermissions() + { + header('Content-Type: application/json'); + + // Only admins can update permissions. + if ( + !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || + !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true + ) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Verify CSRF token from headers. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Get POST input. + $input = json_decode(file_get_contents("php://input"), true); + if (!isset($input['permissions']) || !is_array($input['permissions'])) { + echo json_encode(["error" => "Invalid input"]); + exit; + } + + $permissions = $input['permissions']; + + // Delegate to the model. + $result = userModel::updateUserPermissions($permissions); + echo json_encode($result); + } + + /** + * @OA\Post( + * path="/api/changePassword.php", + * summary="Change user password", + * description="Allows an authenticated user to change their password by verifying the old password and updating to a new one.", + * operationId="changePassword", + * tags={"Users"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"oldPassword", "newPassword", "confirmPassword"}, + * @OA\Property(property="oldPassword", type="string", example="oldpass123"), + * @OA\Property(property="newPassword", type="string", example="newpass456"), + * @OA\Property(property="confirmPassword", type="string", example="newpass456") + * ) + * ), + * @OA\Response( + * response=200, + * description="Password updated successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="Password updated successfully.") + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ) + * ) + */ + + public function changePassword() + { + header('Content-Type: application/json'); + + // Ensure user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + $username = $_SESSION['username'] ?? ''; + if (!$username) { + echo json_encode(["error" => "No username in session"]); + exit; + } + + // CSRF token check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if ($receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Get POST data. + $data = json_decode(file_get_contents("php://input"), true); + $oldPassword = trim($data["oldPassword"] ?? ""); + $newPassword = trim($data["newPassword"] ?? ""); + $confirmPassword = trim($data["confirmPassword"] ?? ""); + + // Validate input. + if (!$oldPassword || !$newPassword || !$confirmPassword) { + echo json_encode(["error" => "All fields are required."]); + exit; + } + if ($newPassword !== $confirmPassword) { + echo json_encode(["error" => "New passwords do not match."]); + exit; + } + + // Delegate password change logic to the model. + $result = userModel::changePassword($username, $oldPassword, $newPassword); + echo json_encode($result); + } + + /** + * @OA\Put( + * path="/api/updateUserPanel.php", + * summary="Update user panel settings", + * description="Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.", + * operationId="updateUserPanel", + * tags={"Users"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"totp_enabled"}, + * @OA\Property(property="totp_enabled", type="boolean", example=false) + * ) + * ), + * @OA\Response( + * response=200, + * description="User panel updated successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="string", example="User panel updated: TOTP disabled") + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ) + * ) + */ + + public function updateUserPanel() + { + header('Content-Type: application/json'); + + // Check if the user is authenticated. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(403); + echo json_encode(["error" => "Unauthorized"]); + exit; + } + + // Verify the CSRF token. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Get the POST input. + $data = json_decode(file_get_contents("php://input"), true); + if (!is_array($data)) { + http_response_code(400); + echo json_encode(["error" => "Invalid input"]); + exit; + } + + $username = $_SESSION['username'] ?? ''; + if (!$username) { + http_response_code(400); + echo json_encode(["error" => "No username in session"]); + exit; + } + + // Extract totp_enabled, converting it to boolean. + $totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false; + + // Delegate to the model. + $result = userModel::updateUserPanel($username, $totp_enabled); + echo json_encode($result); + } + + /** + * @OA\Put( + * path="/api/totp_disable.php", + * summary="Disable TOTP for the authenticated user", + * description="Clears the TOTP secret from the users file for the current user.", + * operationId="disableTOTP", + * tags={"TOTP"}, + * @OA\Response( + * response=200, + * description="TOTP disabled successfully", + * @OA\JsonContent( + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="TOTP disabled successfully.") + * ) + * ), + * @OA\Response( + * response=403, + * description="Not authenticated or invalid CSRF token" + * ), + * @OA\Response( + * response=500, + * description="Failed to disable TOTP" + * ) + * ) + */ + + public function disableTOTP() + { + header('Content-Type: application/json'); + + // Authentication check. + if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(403); + echo json_encode(["error" => "Not authenticated"]); + exit; + } + + $username = $_SESSION['username'] ?? ''; + if (empty($username)) { + http_response_code(400); + echo json_encode(["error" => "Username not found in session"]); + exit; + } + + // CSRF token check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + // Delegate the TOTP disabling logic to the model. + $result = userModel::disableTOTPSecret($username); + + if ($result) { + echo json_encode(["success" => true, "message" => "TOTP disabled successfully."]); + } else { + http_response_code(500); + echo json_encode(["error" => "Failed to disable TOTP."]); + } + } + + /** + * @OA\Post( + * path="/api/totp_recover.php", + * summary="Recover TOTP", + * description="Verifies a recovery code to disable TOTP and finalize login.", + * operationId="recoverTOTP", + * tags={"TOTP"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"recovery_code"}, + * @OA\Property(property="recovery_code", type="string", example="ABC123DEF456") + * ) + * ), + * @OA\Response( + * response=200, + * description="Recovery successful", + * @OA\JsonContent( + * @OA\Property(property="status", type="string", example="ok") + * ) + * ), + * @OA\Response( + * response=400, + * description="Invalid input or recovery code" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token" + * ), + * @OA\Response( + * response=405, + * description="Method not allowed" + * ), + * @OA\Response( + * response=429, + * description="Too many attempts" + * ) + * ) + */ + + public function recoverTOTP() + { + header('Content-Type: application/json'); + + // 1) Only allow POST. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + exit(json_encode(['status' => 'error', 'message' => 'Method not allowed'])); + } + + // 2) CSRF check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + http_response_code(403); + exit(json_encode(['status' => 'error', 'message' => 'Invalid CSRF token'])); + } + + // 3) Identify the user. + $userId = $_SESSION['username'] ?? $_SESSION['pending_login_user'] ?? null; + if (!$userId) { + http_response_code(401); + exit(json_encode(['status' => 'error', 'message' => 'Unauthorized'])); + } + + // 4) Validate userId format. + if (!preg_match(REGEX_USER, $userId)) { + http_response_code(400); + exit(json_encode(['status' => 'error', 'message' => 'Invalid user identifier'])); + } + + // 5) Get the recovery code from input. + $inputData = json_decode(file_get_contents("php://input"), true); + $recoveryCode = $inputData['recovery_code'] ?? ''; + + // 6) Delegate to the model. + $result = userModel::recoverTOTP($userId, $recoveryCode); + + if ($result['status'] === 'ok') { + // 7) Finalize login. + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $userId; + unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']); + echo json_encode(['status' => 'ok']); + } else { + // Set appropriate HTTP code for errors. + if ($result['message'] === 'Too many attempts. Try again later.') { + http_response_code(429); + } else { + http_response_code(400); + } + echo json_encode($result); + } + } + + /** + * @OA\Post( + * path="/api/totp_saveCode.php", + * summary="Generate and save a new TOTP recovery code", + * description="Generates a new TOTP recovery code for the authenticated user, stores its hash, and returns the plain text recovery code.", + * operationId="totpSaveCode", + * tags={"TOTP"}, + * @OA\Response( + * response=200, + * description="Recovery code generated successfully", + * @OA\JsonContent( + * @OA\Property(property="status", type="string", example="ok"), + * @OA\Property(property="recoveryCode", type="string", example="ABC123DEF456") + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request" + * ), + * @OA\Response( + * response=403, + * description="Invalid CSRF token or unauthorized" + * ), + * @OA\Response( + * response=405, + * description="Method not allowed" + * ) + * ) + */ + + public function saveTOTPRecoveryCode() + { + header('Content-Type: application/json'); + + // 1) Only allow POST requests. + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + error_log("totp_saveCode: invalid method {$_SERVER['REQUEST_METHOD']}"); + exit(json_encode(['status' => 'error', 'message' => 'Method not allowed'])); + } + + // 2) CSRF token check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + http_response_code(403); + exit(json_encode(['status' => 'error', 'message' => 'Invalid CSRF token'])); + } + + // 3) Ensure the user is authenticated. + if (empty($_SESSION['username'])) { + http_response_code(401); + error_log("totp_saveCode: unauthorized attempt from IP {$_SERVER['REMOTE_ADDR']}"); + exit(json_encode(['status' => 'error', 'message' => 'Unauthorized'])); + } + + // 4) Validate the username format. + $userId = $_SESSION['username']; + if (!preg_match(REGEX_USER, $userId)) { + http_response_code(400); + error_log("totp_saveCode: invalid username format: {$userId}"); + exit(json_encode(['status' => 'error', 'message' => 'Invalid user identifier'])); + } + + // 5) Delegate to the model. + $result = userModel::saveTOTPRecoveryCode($userId); + if ($result['status'] === 'ok') { + echo json_encode($result); + } else { + http_response_code(500); + echo json_encode($result); + } + } + + /** + * @OA\Get( + * path="/api/totp_setup.php", + * summary="Set up TOTP and generate a QR code", + * description="Generates (or retrieves) the TOTP secret for the user and builds a QR code image for scanning.", + * operationId="setupTOTP", + * tags={"TOTP"}, + * @OA\Response( + * response=200, + * description="QR code image for TOTP setup", + * @OA\MediaType( + * mediaType="image/png" + * ) + * ), + * @OA\Response( + * response=403, + * description="Unauthorized or invalid CSRF token" + * ), + * @OA\Response( + * response=500, + * description="Server error" + * ) + * ) + */ + + public function setupTOTP() + { + // Allow access if the user is authenticated or pending TOTP. + if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) { + http_response_code(403); + exit(json_encode(["error" => "Not authorized to access TOTP setup"])); + } + + // Verify CSRF token from headers. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); + exit; + } + + $username = $_SESSION['username'] ?? ''; + if (!$username) { + http_response_code(400); + exit; + } + + // Set header for PNG output. + header("Content-Type: image/png"); + + // Delegate the TOTP setup work to the model. + $result = userModel::setupTOTP($username); + if (isset($result['error'])) { + http_response_code(500); + echo json_encode(["error" => $result['error']]); + exit; + } + + // Output the QR code image. + echo $result['imageData']; + } + + /** + * @OA\Post( + * path="/api/totp_verify.php", + * summary="Verify TOTP code", + * description="Verifies a TOTP code and completes login for pending users or validates TOTP for setup verification.", + * operationId="verifyTOTP", + * tags={"TOTP"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"totp_code"}, + * @OA\Property(property="totp_code", type="string", example="123456") + * ) + * ), + * @OA\Response( + * response=200, + * description="TOTP successfully verified", + * @OA\JsonContent( + * @OA\Property(property="status", type="string", example="ok"), + * @OA\Property(property="message", type="string", example="Login successful") + * ) + * ), + * @OA\Response( + * response=400, + * description="Bad Request (e.g., invalid input)" + * ), + * @OA\Response( + * response=403, + * description="Not authenticated or invalid CSRF token" + * ), + * @OA\Response( + * response=429, + * description="Too many attempts. Try again later." + * ) + * ) + */ + + public function verifyTOTP() + { + header('Content-Type: application/json'); + // Set CSP headers if desired: + header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';"); + + // Rate‑limit: initialize totp_failures if not set. + if (!isset($_SESSION['totp_failures'])) { + $_SESSION['totp_failures'] = 0; + } + if ($_SESSION['totp_failures'] >= 5) { + http_response_code(429); + echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']); + exit; + } + + // Must be authenticated OR have a pending login. + if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Not authenticated']); + exit; + } + + // CSRF check. + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; + if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']); + exit; + } + + // Parse input. + $inputData = json_decode(file_get_contents("php://input"), true); + $code = trim($inputData['totp_code'] ?? ''); + if (!preg_match('/^\d{6}$/', $code)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']); + exit; + } + + // Create TFA object. + $tfa = new \RobThree\Auth\TwoFactorAuth( + new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), + 'FileRise', + 6, + 30, + \RobThree\Auth\Algorithm::Sha1 + ); + + // Check if we are in pending login flow. + if (isset($_SESSION['pending_login_user'])) { + $username = $_SESSION['pending_login_user']; + $pendingSecret = $_SESSION['pending_login_secret'] ?? null; + if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) { + $_SESSION['totp_failures']++; + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); + exit; + } + // Successful pending login: finalize login. + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $username; + // Set isAdmin based on user role. + $_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1"); + // Load additional permissions (e.g., folderOnly) as needed. + $_SESSION['folderOnly'] = loadUserPermissions($username); + unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']); + echo json_encode(['status' => 'ok', 'message' => 'Login successful']); + exit; + } + + // Otherwise, we are in setup/verification flow. + $username = $_SESSION['username'] ?? ''; + if (!$username) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Username not found in session']); + exit; + } + + // Retrieve the user's TOTP secret from the model. + $totpSecret = userModel::getTOTPSecret($username); + if (!$totpSecret) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']); + exit; + } + + if (!$tfa->verifyCode($totpSecret, $code)) { + $_SESSION['totp_failures']++; + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); + exit; + } + + // Successful verification. + unset($_SESSION['totp_failures']); + echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']); + } +} diff --git a/src/models/AdminModel.php b/src/models/AdminModel.php new file mode 100644 index 0000000..c446c8d --- /dev/null +++ b/src/models/AdminModel.php @@ -0,0 +1,118 @@ + "Incomplete OIDC configuration."]; + } + + // Convert configuration to JSON. + $plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT); + if ($plainTextConfig === false) { + return ["error" => "Failed to encode configuration to JSON."]; + } + + // Encrypt configuration. + $encryptedContent = encryptData($plainTextConfig, $GLOBALS['encryptionKey']); + if ($encryptedContent === false) { + return ["error" => "Failed to encrypt configuration."]; + } + + // Define the configuration file path. + $configFile = USERS_DIR . 'adminConfig.json'; + + // Attempt to write the new configuration. + if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) { + // Attempt a cleanup: delete the old file and try again. + if (file_exists($configFile)) { + unlink($configFile); + } + if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) { + error_log("AdminModel::updateConfig: Failed to write configuration even after deletion."); + return ["error" => "Failed to update configuration even after cleanup."]; + } + } + + return ["success" => "Configuration updated successfully."]; + } + + /** + * Retrieves the current configuration. + * + * @return array The configuration array, or defaults if not found. + */ + public static function getConfig(): array { + $configFile = USERS_DIR . 'adminConfig.json'; + if (file_exists($configFile)) { + $encryptedContent = file_get_contents($configFile); + $decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']); + if ($decryptedContent === false) { + http_response_code(500); + return ["error" => "Failed to decrypt configuration."]; + } + $config = json_decode($decryptedContent, true); + if (!is_array($config)) { + $config = []; + } + + // Normalize login options. + if (!isset($config['loginOptions'])) { + // Create loginOptions array from top-level keys if missing. + $config['loginOptions'] = [ + 'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false, + 'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false, + 'disableOIDCLogin' => isset($config['disableOIDCLogin']) ? (bool)$config['disableOIDCLogin'] : false, + ]; + unset($config['disableFormLogin'], $config['disableBasicAuth'], $config['disableOIDCLogin']); + } else { + // Ensure proper boolean types + $config['loginOptions']['disableFormLogin'] = (bool)$config['loginOptions']['disableFormLogin']; + $config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth']; + $config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin']; + } + + if (!isset($config['globalOtpauthUrl'])) { + $config['globalOtpauthUrl'] = ""; + } + if (!isset($config['header_title']) || empty($config['header_title'])) { + $config['header_title'] = "FileRise"; + } + return $config; + } else { + // Return defaults. + return [ + 'header_title' => "FileRise", + 'oidc' => [ + 'providerUrl' => 'https://your-oidc-provider.com', + 'clientId' => 'YOUR_CLIENT_ID', + 'clientSecret' => 'YOUR_CLIENT_SECRET', + 'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback' + ], + 'loginOptions' => [ + 'disableFormLogin' => false, + 'disableBasicAuth' => false, + 'disableOIDCLogin' => false + ], + 'globalOtpauthUrl' => "" + ]; + } + } +} diff --git a/src/models/AuthModel.php b/src/models/AuthModel.php new file mode 100644 index 0000000..37c8cf8 --- /dev/null +++ b/src/models/AuthModel.php @@ -0,0 +1,124 @@ += 3 && $parts[0] === $username) { + return trim($parts[2]); + } + } + } + return null; + } + + /** + * Authenticates the user using form-based credentials. + * + * @param string $username + * @param string $password + * @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure. + */ + public static function authenticate(string $username, string $password) { + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) { + return false; + } + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + if (count($parts) < 3) continue; + if ($username === $parts[0] && password_verify($password, $parts[1])) { + return [ + 'role' => $parts[2], + 'totp_secret' => (isset($parts[3]) && !empty($parts[3])) + ? decryptData($parts[3], $GLOBALS['encryptionKey']) + : null + ]; + } + } + return false; + } + + /** + * Loads failed login attempts from a file. + * + * @param string $file + * @return array + */ + public static function loadFailedAttempts(string $file): array { + if (file_exists($file)) { + $data = json_decode(file_get_contents($file), true); + if (is_array($data)) { + return $data; + } + } + return []; + } + + /** + * Saves failed login attempts into a file. + * + * @param string $file + * @param array $data + * @return void + */ + public static function saveFailedAttempts(string $file, array $data): void { + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX); + } + + /** + * Retrieves a user's TOTP secret from the users file. + * + * @param string $username + * @return string|null Returns the decrypted TOTP secret or null if not set. + */ + public static function getUserTOTPSecret(string $username): ?string { + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) { + return null; + } + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + return decryptData($parts[3], $GLOBALS['encryptionKey']); + } + } + return null; + } + + /** + * Loads the folder-only permission for a given user. + * + * @param string $username + * @return bool + */ + public static function loadFolderPermission(string $username): bool { + $permissionsFile = USERS_DIR . 'userPermissions.json'; + if (file_exists($permissionsFile)) { + $content = file_get_contents($permissionsFile); + $decrypted = decryptData($content, $GLOBALS['encryptionKey']); + $permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true); + if (is_array($permissions)) { + foreach ($permissions as $storedUsername => $data) { + if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) { + return (bool)$data['folderOnly']; + } + } + } + } + return false; + } +} \ No newline at end of file diff --git a/src/models/FileModel.php b/src/models/FileModel.php new file mode 100644 index 0000000..0dae96a --- /dev/null +++ b/src/models/FileModel.php @@ -0,0 +1,1249 @@ + "Files copied successfully"]; + } else { + return ["error" => implode("; ", $errors)]; + } + } + + /** + * Generates the metadata file path for a given folder. + * + * @param string $folder + * @return string + */ + private static function getMetadataFilePath($folder) { + if (strtolower($folder) === 'root' || trim($folder) === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; + } + + /** + * Generates a unique file name if a file with the same name exists in the destination directory. + * + * @param string $destDir + * @param string $fileName + * @return string + */ + private static function getUniqueFileName($destDir, $fileName) { + $fullPath = $destDir . $fileName; + clearstatcache(true, $fullPath); + if (!file_exists($fullPath)) { + return $fileName; + } + $basename = pathinfo($fileName, PATHINFO_FILENAME); + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + $counter = 1; + do { + $newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : ""); + $newFullPath = $destDir . $newName; + clearstatcache(true, $newFullPath); + $counter++; + } while (file_exists($destDir . $newName)); + return $newName; + } + + /** + * Deletes (i.e. moves to Trash) the specified files from a given folder + * and updates metadata accordingly. + * + * @param string $folder The folder (or "root") from which files are deleted. + * @param array $files The array of file names to delete. + * @return array An associative array with a "success" or "error" message. + */ + public static function deleteFiles($folder, $files) { + $errors = []; + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + + // Determine the upload directory. + $uploadDir = ($folder === 'root') + ? $baseDir . DIRECTORY_SEPARATOR + : $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR; + + // Setup the Trash folder and metadata. + $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; + if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); + } + $trashMetadataFile = $trashDir . "trash.json"; + $trashData = file_exists($trashMetadataFile) + ? json_decode(file_get_contents($trashMetadataFile), true) + : []; + if (!is_array($trashData)) { + $trashData = []; + } + + // Load folder metadata if available. + $metadataFile = self::getMetadataFilePath($folder); + $folderMetadata = file_exists($metadataFile) + ? json_decode(file_get_contents($metadataFile), true) + : []; + if (!is_array($folderMetadata)) { + $folderMetadata = []; + } + + $movedFiles = []; + // Define a safe file name pattern. + $safeFileNamePattern = REGEX_FILE_NAME; + + foreach ($files as $fileName) { + $basename = basename(trim($fileName)); + + // Validate the file name. + if (!preg_match($safeFileNamePattern, $basename)) { + $errors[] = "$basename has an invalid name."; + continue; + } + + $filePath = $uploadDir . $basename; + + // Check if file exists. + if (file_exists($filePath)) { + // Append a timestamp to create a unique trash file name. + $timestamp = time(); + $trashFileName = $basename . "_" . $timestamp; + if (rename($filePath, $trashDir . $trashFileName)) { + $movedFiles[] = $basename; + // Record trash metadata for possible restoration. + $trashData[] = [ + 'type' => 'file', + 'originalFolder' => $uploadDir, + 'originalName' => $basename, + 'trashName' => $trashFileName, + 'trashedAt' => $timestamp, + 'uploaded' => isset($folderMetadata[$basename]['uploaded']) + ? $folderMetadata[$basename]['uploaded'] : "Unknown", + 'uploader' => isset($folderMetadata[$basename]['uploader']) + ? $folderMetadata[$basename]['uploader'] : "Unknown", + 'deletedBy' => $_SESSION['username'] ?? "Unknown" + ]; + } else { + $errors[] = "Failed to move $basename to Trash."; + continue; + } + } else { + // If file does not exist, consider it already removed. + $movedFiles[] = $basename; + } + } + + // Save updated trash metadata. + file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT)); + + // Remove deleted file entries from folder metadata. + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (is_array($metadata)) { + foreach ($movedFiles as $delFile) { + if (isset($metadata[$delFile])) { + unset($metadata[$delFile]); + } + } + file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); + } + } + + if (empty($errors)) { + return ["success" => "Files moved to Trash: " . implode(", ", $movedFiles)]; + } else { + return ["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)]; + } + } + + /** + * Moves files from a source folder to a destination folder and updates metadata. + * + * @param string $sourceFolder The source folder (e.g., "root" or a subfolder). + * @param string $destinationFolder The destination folder. + * @param array $files An array of file names to move. + * @return array An associative array with either a "success" key or an "error" key. + */ + public static function moveFiles($sourceFolder, $destinationFolder, $files) { + $errors = []; + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + + // Build source and destination directories. + $sourceDir = ($sourceFolder === 'root') + ? $baseDir . DIRECTORY_SEPARATOR + : $baseDir . DIRECTORY_SEPARATOR . trim($sourceFolder, "/\\ ") . DIRECTORY_SEPARATOR; + $destDir = ($destinationFolder === 'root') + ? $baseDir . DIRECTORY_SEPARATOR + : $baseDir . DIRECTORY_SEPARATOR . trim($destinationFolder, "/\\ ") . DIRECTORY_SEPARATOR; + + // Ensure destination directory exists. + if (!is_dir($destDir)) { + if (!mkdir($destDir, 0775, true)) { + return ["error" => "Could not create destination folder"]; + } + } + + // Get metadata file paths. + $srcMetaFile = self::getMetadataFilePath($sourceFolder); + $destMetaFile = self::getMetadataFilePath($destinationFolder); + + $srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : []; + $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; + if (!is_array($srcMetadata)) { + $srcMetadata = []; + } + if (!is_array($destMetadata)) { + $destMetadata = []; + } + + $movedFiles = []; + // Define a safe file name pattern. + $safeFileNamePattern = REGEX_FILE_NAME; + + foreach ($files as $fileName) { + // Save the original file name for metadata lookup. + $originalName = basename(trim($fileName)); + $basename = $originalName; + + // Validate the file name. + if (!preg_match($safeFileNamePattern, $basename)) { + $errors[] = "$basename has invalid characters."; + continue; + } + + $srcPath = $sourceDir . $originalName; + $destPath = $destDir . $basename; + + clearstatcache(); + if (!file_exists($srcPath)) { + $errors[] = "$originalName does not exist in source."; + continue; + } + + // If a file with the same name exists in destination, generate a unique name. + if (file_exists($destPath)) { + $uniqueName = self::getUniqueFileName($destDir, $basename); + $basename = $uniqueName; + $destPath = $destDir . $uniqueName; + } + + if (!rename($srcPath, $destPath)) { + $errors[] = "Failed to move $basename."; + continue; + } + + $movedFiles[] = $originalName; + // Update destination metadata: if metadata for the original file exists in source, move it under the new name. + if (isset($srcMetadata[$originalName])) { + $destMetadata[$basename] = $srcMetadata[$originalName]; + unset($srcMetadata[$originalName]); + } + } + + // Write back updated metadata. + if (file_put_contents($srcMetaFile, json_encode($srcMetadata, JSON_PRETTY_PRINT)) === false) { + $errors[] = "Failed to update source metadata."; + } + if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { + $errors[] = "Failed to update destination metadata."; + } + + if (empty($errors)) { + return ["success" => "Files moved successfully"]; + } else { + return ["error" => implode("; ", $errors)]; + } + } + + /** + * Renames a file within a given folder and updates folder metadata. + * + * @param string $folder The folder where the file is located (or "root" for the base directory). + * @param string $oldName The current name of the file. + * @param string $newName The new name for the file. + * @return array An associative array with either "success" (and newName) or "error" message. + */ + public static function renameFile($folder, $oldName, $newName) { + // Determine the directory path. + $directory = ($folder !== 'root') + ? rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR + : UPLOAD_DIR; + + // Sanitize file names. + $oldName = basename(trim($oldName)); + $newName = basename(trim($newName)); + + // Validate file names using REGEX_FILE_NAME. + if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) { + return ["error" => "Invalid file name."]; + } + + $oldPath = $directory . $oldName; + $newPath = $directory . $newName; + + // Helper: Generate a unique file name if the new name already exists. + if (file_exists($newPath)) { + $newName = self::getUniqueFileName($directory, $newName); + $newPath = $directory . $newName; + } + + // Check that the old file exists. + if (!file_exists($oldPath)) { + return ["error" => "File does not exist"]; + } + + // Perform the rename. + if (rename($oldPath, $newPath)) { + // Update the metadata file. + $metadataKey = ($folder === 'root') ? "root" : $folder; + $metadataFile = META_DIR . str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json'; + + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (isset($metadata[$oldName])) { + $metadata[$newName] = $metadata[$oldName]; + unset($metadata[$oldName]); + file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); + } + } + return ["success" => "File renamed successfully", "newName" => $newName]; + } else { + return ["error" => "Error renaming file"]; + } + } + + /** + * Saves file content to disk and updates folder metadata. + * + * @param string $folder The target folder where the file is to be saved (e.g. "root" or a subfolder). + * @param string $fileName The name of the file. + * @param string $content The file content. + * @return array Returns an associative array with either a "success" key or an "error" key. + */ + public static function saveFile($folder, $fileName, $content) { + // Sanitize and determine the folder name. + $folder = trim($folder) ?: 'root'; + $fileName = basename(trim($fileName)); + + // Validate folder: if not "root", must match REGEX_FOLDER_NAME. + if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name"]; + } + + // Determine base upload directory. + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + if (strtolower($folder) === 'root' || $folder === "") { + $targetDir = $baseDir . DIRECTORY_SEPARATOR; + } else { + $targetDir = $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR; + } + + // (Optional security check to ensure targetDir is within baseDir.) + if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) { + return ["error" => "Invalid folder path"]; + } + + // Create target directory if it doesn't exist. + if (!is_dir($targetDir)) { + if (!mkdir($targetDir, 0775, true)) { + return ["error" => "Failed to create destination folder"]; + } + } + + $filePath = $targetDir . $fileName; + // Attempt to save the file. + if (file_put_contents($filePath, $content) === false) { + return ["error" => "Error saving file"]; + } + + // Update metadata. + // Build metadata file path for the folder. + $metadataKey = (strtolower($folder) === "root" || $folder === "") ? "root" : $folder; + $metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json'; + $metadataFilePath = META_DIR . $metadataFileName; + + if (file_exists($metadataFilePath)) { + $metadata = json_decode(file_get_contents($metadataFilePath), true); + } else { + $metadata = []; + } + if (!is_array($metadata)) { + $metadata = []; + } + + $currentTime = date(DATE_TIME_FORMAT); + $uploader = $_SESSION['username'] ?? "Unknown"; + + // Update metadata for the file. If already exists, update its "modified" timestamp. + if (isset($metadata[$fileName])) { + $metadata[$fileName]['modified'] = $currentTime; + $metadata[$fileName]['uploader'] = $uploader; // optional: update uploader if desired. + } else { + $metadata[$fileName] = [ + "uploaded" => $currentTime, + "modified" => $currentTime, + "uploader" => $uploader + ]; + } + + // Write updated metadata. + if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)) === false) { + return ["error" => "Failed to update metadata"]; + } + + return ["success" => "File saved successfully"]; + } + + /** + * Validates and retrieves information needed to download a file. + * + * @param string $folder The folder from which to download (e.g., "root" or a subfolder). + * @param string $file The file name. + * @return array An associative array with "error" key on failure, + * or "filePath" and "mimeType" keys on success. + */ + public static function getDownloadInfo($folder, $file) { + // Validate file name using REGEX_FILE_NAME. + $file = basename(trim($file)); + if (!preg_match(REGEX_FILE_NAME, $file)) { + return ["error" => "Invalid file name."]; + } + + // Determine the real upload directory. + $uploadDirReal = realpath(UPLOAD_DIR); + if ($uploadDirReal === false) { + return ["error" => "Server misconfiguration."]; + } + + // Determine directory based on folder. + if (strtolower($folder) === 'root' || trim($folder) === '') { + $directory = $uploadDirReal; + } else { + // Prevent path traversal. + if (strpos($folder, '..') !== false) { + return ["error" => "Invalid folder name."]; + } + $directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ "); + $directory = realpath($directoryPath); + if ($directory === false || strpos($directory, $uploadDirReal) !== 0) { + return ["error" => "Invalid folder path."]; + } + } + + // Build the file path. + $filePath = $directory . DIRECTORY_SEPARATOR . $file; + $realFilePath = realpath($filePath); + + // Ensure the file exists and is within the allowed directory. + if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) { + return ["error" => "Access forbidden."]; + } + if (!file_exists($realFilePath)) { + return ["error" => "File not found."]; + } + + // Get the MIME type. + $mimeType = mime_content_type($realFilePath); + return [ + "filePath" => $realFilePath, + "mimeType" => $mimeType + ]; + } + + /** + * Creates a ZIP archive of the specified files from a given folder. + * + * @param string $folder The folder from which to zip the files (e.g., "root" or a subfolder). + * @param array $files An array of file names to include in the ZIP. + * @return array An associative array with either an "error" key or a "zipPath" key. + */ + public static function createZipArchive($folder, $files) { + // Validate and build folder path. + $folder = trim($folder) ?: 'root'; + $baseDir = realpath(UPLOAD_DIR); + if ($baseDir === false) { + return ["error" => "Uploads directory not configured correctly."]; + } + if (strtolower($folder) === 'root' || $folder === "") { + $folderPathReal = $baseDir; + } else { + // Prevent path traversal. + if (strpos($folder, '..') !== false) { + return ["error" => "Invalid folder name."]; + } + $folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ "); + $folderPathReal = realpath($folderPath); + if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { + return ["error" => "Folder not found."]; + } + } + + // Validate each file and build an array of files to zip. + $filesToZip = []; + foreach ($files as $fileName) { + // Validate file name using REGEX_FILE_NAME. + $fileName = basename(trim($fileName)); + if (!preg_match(REGEX_FILE_NAME, $fileName)) { + continue; + } + $fullPath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName; + if (file_exists($fullPath)) { + $filesToZip[] = $fullPath; + } + } + if (empty($filesToZip)) { + return ["error" => "No valid files found to zip."]; + } + + // Create a temporary ZIP file. + $tempZip = tempnam(sys_get_temp_dir(), 'zip'); + unlink($tempZip); // Remove the temp file so that ZipArchive can create a new file. + $tempZip .= '.zip'; + + $zip = new ZipArchive(); + if ($zip->open($tempZip, ZipArchive::CREATE) !== TRUE) { + return ["error" => "Could not create zip archive."]; + } + // Add each file using its base name. + foreach ($filesToZip as $filePath) { + $zip->addFile($filePath, basename($filePath)); + } + $zip->close(); + + return ["zipPath" => $tempZip]; + } + + /** + * Extracts ZIP archives from the specified folder. + * + * @param string $folder The folder from which ZIP files will be extracted (e.g., "root" or a subfolder). + * @param array $files An array of ZIP file names to extract. + * @return array An associative array with keys "success" (boolean), and either "extractedFiles" (array) on success or "error" (string) on failure. + */ + public static function extractZipArchive($folder, $files) { + $errors = []; + $allSuccess = true; + $extractedFiles = []; + + // Determine the base upload directory and build the folder path. + $baseDir = realpath(UPLOAD_DIR); + if ($baseDir === false) { + return ["error" => "Uploads directory not configured correctly."]; + } + + if (strtolower($folder) === "root" || trim($folder) === "") { + $relativePath = ""; + } else { + $parts = explode('/', trim($folder, "/\\")); + foreach ($parts as $part) { + if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) { + return ["error" => "Invalid folder name."]; + } + } + $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR; + } + + $folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath; + $folderPathReal = realpath($folderPath); + if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { + return ["error" => "Folder not found."]; + } + + // Prepare metadata. + // Reuse our helper method if available; otherwise, re-create the logic. + $metadataFile = self::getMetadataFilePath($folder); + $srcMetadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + if (!is_array($srcMetadata)) { + $srcMetadata = []; + } + // For simplicity, we update the same metadata file after extraction. + $destMetadata = $srcMetadata; + + // Define a safe file name pattern. + $safeFileNamePattern = REGEX_FILE_NAME; + + // Process each ZIP file. + foreach ($files as $zipFileName) { + $originalName = basename(trim($zipFileName)); + // Process only .zip files. + if (strtolower(substr($originalName, -4)) !== '.zip') { + continue; + } + if (!preg_match($safeFileNamePattern, $originalName)) { + $errors[] = "$originalName has an invalid name."; + $allSuccess = false; + continue; + } + + $zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName; + if (!file_exists($zipFilePath)) { + $errors[] = "$originalName does not exist in folder."; + $allSuccess = false; + continue; + } + + $zip = new ZipArchive(); + if ($zip->open($zipFilePath) !== TRUE) { + $errors[] = "Could not open $originalName as a zip file."; + $allSuccess = false; + continue; + } + + // Attempt extraction. + if (!$zip->extractTo($folderPathReal)) { + $errors[] = "Failed to extract $originalName."; + $allSuccess = false; + } else { + // Collect extracted file names from this archive. + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + $extractedFileName = basename($entryName); + if ($extractedFileName) { + $extractedFiles[] = $extractedFileName; + } + } + // Update metadata for each extracted file if the ZIP has metadata. + if (isset($srcMetadata[$originalName])) { + $zipMeta = $srcMetadata[$originalName]; + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + $extractedFileName = basename($entryName); + if ($extractedFileName) { + $destMetadata[$extractedFileName] = $zipMeta; + } + } + } + } + $zip->close(); + } + + // Save updated metadata. + if (file_put_contents($metadataFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { + $errors[] = "Failed to update metadata."; + $allSuccess = false; + } + + if ($allSuccess) { + return ["success" => true, "extractedFiles" => $extractedFiles]; + } else { + return ["success" => false, "error" => implode(" ", $errors)]; + } + } + + /** + * Retrieves the share record for a given token. + * + * @param string $token The share token. + * @return array|null Returns the share record as an associative array, or null if not found. + */ + public static function getShareRecord($token) { + $shareFile = META_DIR . "share_links.json"; + if (!file_exists($shareFile)) { + return null; + } + $shareLinks = json_decode(file_get_contents($shareFile), true); + if (!is_array($shareLinks) || !isset($shareLinks[$token])) { + return null; + } + return $shareLinks[$token]; + } + + /** + * Creates a share link for a file. + * + * @param string $folder The folder containing the shared file (or "root"). + * @param string $file The name of the file being shared. + * @param int $expirationMinutes The number of minutes until expiration. + * @param string $password Optional password protecting the share. + * @return array Returns an associative array with keys "token" and "expires" on success, + * or "error" on failure. + */ + public static function createShareLink($folder, $file, $expirationMinutes = 60, $password = "") { + // Validate folder if necessary (this can also be done in the controller). + if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name."]; + } + + // Generate a secure token (32 hex characters). + $token = bin2hex(random_bytes(16)); + + // Calculate expiration (Unix timestamp). + $expires = time() + ($expirationMinutes * 60); + + // Hash the password if provided. + $hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : ""; + + // File to store share links. + $shareFile = META_DIR . "share_links.json"; + $shareLinks = []; + if (file_exists($shareFile)) { + $data = file_get_contents($shareFile); + $shareLinks = json_decode($data, true); + if (!is_array($shareLinks)) { + $shareLinks = []; + } + } + + // Clean up expired share links. + $currentTime = time(); + foreach ($shareLinks as $key => $link) { + if ($link["expires"] < $currentTime) { + unset($shareLinks[$key]); + } + } + + // Add new share record. + $shareLinks[$token] = [ + "folder" => $folder, + "file" => $file, + "expires" => $expires, + "password" => $hashedPassword + ]; + + // Save the updated share links. + if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) { + return ["token" => $token, "expires" => $expires]; + } else { + return ["error" => "Could not save share link."]; + } + } + + /** + * Retrieves and enriches trash records from the trash metadata file. + * + * @return array An array of trash items. + */ + public static function getTrashItems() { + $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; + $trashMetadataFile = $trashDir . "trash.json"; + $trashItems = []; + if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $trashItems = json_decode($json, true); + if (!is_array($trashItems)) { + $trashItems = []; + } + } + + // Enrich each trash record. + foreach ($trashItems as &$item) { + if (empty($item['deletedBy'])) { + $item['deletedBy'] = "Unknown"; + } + if (empty($item['uploaded']) || empty($item['uploader'])) { + if (isset($item['originalFolder']) && isset($item['originalName'])) { + $metadataFile = self::getMetadataFilePath($item['originalFolder']); + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (is_array($metadata) && isset($metadata[$item['originalName']])) { + $item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown"; + $item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown"; + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } + } + unset($item); + return $trashItems; + } + + /** + * Restores files from Trash based on an array of trash file identifiers. + * + * @param array $trashFiles An array of trash file names (i.e. the 'trashName' fields). + * @return array An associative array with keys "restored" (an array of successfully restored items) + * and optionally an "error" message if any issues occurred. + */ + public static function restoreFiles(array $trashFiles) { + $errors = []; + $restoredItems = []; + + // Setup Trash directory and trash metadata file. + $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; + if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); + } + $trashMetadataFile = $trashDir . "trash.json"; + $trashData = []; + if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $trashData = json_decode($json, true); + if (!is_array($trashData)) { + $trashData = []; + } + } + + // Helper to get metadata file path for a folder. + $getMetadataFilePath = function($folder) { + if (strtolower($folder) === 'root' || trim($folder) === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; + }; + + // Process each provided trash file name. + foreach ($trashFiles as $trashFileName) { + $trashFileName = trim($trashFileName); + // Validate file name with REGEX_FILE_NAME. + if (!preg_match(REGEX_FILE_NAME, $trashFileName)) { + $errors[] = "$trashFileName has an invalid format."; + continue; + } + + // Locate the matching trash record. + $recordKey = null; + foreach ($trashData as $key => $record) { + if (isset($record['trashName']) && $record['trashName'] === $trashFileName) { + $recordKey = $key; + break; + } + } + if ($recordKey === null) { + $errors[] = "No trash record found for $trashFileName."; + continue; + } + + $record = $trashData[$recordKey]; + if (!isset($record['originalFolder']) || !isset($record['originalName'])) { + $errors[] = "Incomplete trash record for $trashFileName."; + continue; + } + $originalFolder = $record['originalFolder']; + $originalName = $record['originalName']; + + // Convert absolute original folder to relative folder. + $relativeFolder = 'root'; + if (strpos($originalFolder, UPLOAD_DIR) === 0) { + $relativeFolder = trim(substr($originalFolder, strlen(UPLOAD_DIR)), '/\\'); + if ($relativeFolder === '') { + $relativeFolder = 'root'; + } + } + + // Build destination path. + $destinationPath = (strtolower($relativeFolder) !== 'root') + ? rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $relativeFolder . DIRECTORY_SEPARATOR . $originalName + : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $originalName; + + // Handle folder-type records if necessary. + if (isset($record['type']) && $record['type'] === 'folder') { + if (!file_exists($destinationPath)) { + if (mkdir($destinationPath, 0755, true)) { + $restoredItems[] = $originalName . " (folder restored)"; + } else { + $errors[] = "Failed to restore folder $originalName."; + continue; + } + } else { + $errors[] = "Folder already exists at destination: $originalName."; + continue; + } + unset($trashData[$recordKey]); + continue; + } + + // For files: Ensure destination directory exists. + $destinationDir = dirname($destinationPath); + if (!file_exists($destinationDir)) { + if (!mkdir($destinationDir, 0755, true)) { + $errors[] = "Failed to create destination folder for $originalName."; + continue; + } + } + + if (file_exists($destinationPath)) { + $errors[] = "File already exists at destination: $originalName."; + continue; + } + + // Move the file from trash to its original location. + $sourcePath = $trashDir . $trashFileName; + if (file_exists($sourcePath)) { + if (rename($sourcePath, $destinationPath)) { + $restoredItems[] = $originalName; + + // Update metadata: Restore metadata for this file. + $metadataFile = $getMetadataFilePath($relativeFolder); + $metadata = []; + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (!is_array($metadata)) { + $metadata = []; + } + } + $restoredMeta = [ + "uploaded" => isset($record['uploaded']) ? $record['uploaded'] : date(DATE_TIME_FORMAT), + "uploader" => isset($record['uploader']) ? $record['uploader'] : "Unknown" + ]; + $metadata[$originalName] = $restoredMeta; + file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); + unset($trashData[$recordKey]); + } else { + $errors[] = "Failed to restore $originalName."; + } + } else { + $errors[] = "Trash file not found: $trashFileName."; + } + } + + // Write back updated trash metadata. + file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); + + if (empty($errors)) { + return ["success" => "Items restored: " . implode(", ", $restoredItems), "restored" => $restoredItems]; + } else { + return ["success" => false, "error" => implode("; ", $errors), "restored" => $restoredItems]; + } + } + + /** + * Deletes trash items based on an array of trash file identifiers. + * + * @param array $filesToDelete An array of trash file names (identifiers). + * @return array An associative array containing "deleted" (array of deleted items) and optionally "error" (error message). + */ + public static function deleteTrashFiles(array $filesToDelete) { + // Setup trash directory and metadata file. + $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; + if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); + } + $trashMetadataFile = $trashDir . "trash.json"; + + // Load trash metadata into an associative array keyed by trashName. + $trashData = []; + if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $tempData = json_decode($json, true); + if (is_array($tempData)) { + foreach ($tempData as $item) { + if (isset($item['trashName'])) { + $trashData[$item['trashName']] = $item; + } + } + } + } + + $deletedFiles = []; + $errors = []; + + // Define a safe file name pattern. + $safeFileNamePattern = REGEX_FILE_NAME; + + // Process each file identifier in the $filesToDelete array. + foreach ($filesToDelete as $trashName) { + $trashName = trim($trashName); + if (!preg_match($safeFileNamePattern, $trashName)) { + $errors[] = "$trashName has an invalid format."; + continue; + } + if (!isset($trashData[$trashName])) { + $errors[] = "Trash item $trashName not found."; + continue; + } + // Build the full path to the trash file. + $filePath = $trashDir . $trashName; + if (file_exists($filePath)) { + if (unlink($filePath)) { + $deletedFiles[] = $trashName; + unset($trashData[$trashName]); + } else { + $errors[] = "Failed to delete $trashName."; + } + } else { + // If the file doesn't exist, remove its metadata. + unset($trashData[$trashName]); + $deletedFiles[] = $trashName; + } + } + + // Save the updated trash metadata back as an indexed array. + file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); + + if (empty($errors)) { + return ["deleted" => $deletedFiles]; + } else { + return ["deleted" => $deletedFiles, "error" => implode("; ", $errors)]; + } + } + + /** + * Retrieves file tags from the createdTags.json metadata file. + * + * @return array An array of tags. Returns an empty array if the file doesn't exist or is not readable. + */ + public static function getFileTags(): array { + $metadataPath = META_DIR . 'createdTags.json'; + + // Check if the metadata file exists and is readable. + if (!file_exists($metadataPath) || !is_readable($metadataPath)) { + error_log('Metadata file does not exist or is not readable: ' . $metadataPath); + return []; + } + + $data = file_get_contents($metadataPath); + if ($data === false) { + error_log('Failed to read metadata file: ' . $metadataPath); + // Return an empty array for a graceful fallback. + return []; + } + + $jsonData = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + error_log('Invalid JSON in metadata file: ' . $metadataPath . ' Error: ' . json_last_error_msg()); + return []; + } + + return $jsonData; + } + + /** + * Saves tag data for a specified file and updates the global tags. + * + * @param string $folder The folder where the file is located (e.g., "root" or a subfolder). + * @param string $file The name of the file for which tags are being saved. + * @param array $tags An array of tag definitions, each being an associative array (e.g. ['name' => 'Tag1', 'color' => '#FF0000']). + * @param bool $deleteGlobal Optional flag; if true and 'tagToDelete' is provided, remove that tag from the global tags. + * @param string|null $tagToDelete Optional tag name to delete from global tags when $deleteGlobal is true. + * @return array Returns an associative array with a "success" key and updated "globalTags", or an "error" key on failure. + */ + public static function saveFileTag(string $folder, string $file, array $tags, bool $deleteGlobal = false, ?string $tagToDelete = null): array { + // Determine the folder metadata file. + $folder = trim($folder) ?: 'root'; + $metadataFile = ""; + if (strtolower($folder) === "root") { + $metadataFile = META_DIR . "root_metadata.json"; + } else { + $metadataFile = META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; + } + + // Load existing metadata for this folder. + $metadata = []; + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true) ?? []; + } + + // Update the metadata for the specified file. + if (!isset($metadata[$file])) { + $metadata[$file] = []; + } + $metadata[$file]['tags'] = $tags; + + if (file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)) === false) { + return ["error" => "Failed to save tag data for file metadata."]; + } + + // Now update the global tags file. + $globalTagsFile = META_DIR . "createdTags.json"; + $globalTags = []; + if (file_exists($globalTagsFile)) { + $globalTags = json_decode(file_get_contents($globalTagsFile), true) ?? []; + if (!is_array($globalTags)) { + $globalTags = []; + } + } + + // If deleteGlobal is true and tagToDelete is provided, remove that tag. + if ($deleteGlobal && !empty($tagToDelete)) { + $tagToDeleteLower = strtolower($tagToDelete); + $globalTags = array_values(array_filter($globalTags, function($globalTag) use ($tagToDeleteLower) { + return strtolower($globalTag['name']) !== $tagToDeleteLower; + })); + } else { + // Otherwise, merge (update or add) new tags into the global tags. + foreach ($tags as $tag) { + $found = false; + foreach ($globalTags as &$globalTag) { + if (strtolower($globalTag['name']) === strtolower($tag['name'])) { + $globalTag['color'] = $tag['color']; + $found = true; + break; + } + } + if (!$found) { + $globalTags[] = $tag; + } + } + } + + if (file_put_contents($globalTagsFile, json_encode($globalTags, JSON_PRETTY_PRINT)) === false) { + return ["error" => "Failed to save global tags."]; + } + + return ["success" => "Tag data saved successfully.", "globalTags" => $globalTags]; + } + + /** + * Retrieves the list of files in a given folder, enriched with metadata, along with global tags. + * + * @param string $folder The folder name (e.g., "root" or a subfolder). + * @return array Returns an associative array with keys "files" and "globalTags". + */ + public static function getFileList(string $folder): array { + $folder = trim($folder) ?: 'root'; + // Determine the target directory. + if (strtolower($folder) !== 'root') { + $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; + } else { + $directory = UPLOAD_DIR; + } + + // Validate folder. + if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name."]; + } + + // Helper: Build the metadata file path. + $getMetadataFilePath = function(string $folder): string { + if (strtolower($folder) === 'root' || trim($folder) === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; + }; + $metadataFile = $getMetadataFilePath($folder); + $metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + + if (!is_dir($directory)) { + return ["error" => "Directory not found."]; + } + + $allFiles = array_values(array_diff(scandir($directory), array('.', '..'))); + $fileList = []; + + // Define a safe file name pattern. + $safeFileNamePattern = REGEX_FILE_NAME; + + foreach ($allFiles as $file) { + if (substr($file, 0, 1) === '.') { + continue; // Skip hidden files. + } + + $filePath = $directory . DIRECTORY_SEPARATOR . $file; + if (!is_file($filePath)) { + continue; // Only process files. + } + if (!preg_match($safeFileNamePattern, $file)) { + continue; + } + + $fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown"; + $metaKey = $file; + $fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown"; + $fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown"; + + $fileSizeBytes = filesize($filePath); + if ($fileSizeBytes >= 1073741824) { + $fileSizeFormatted = sprintf("%.1f GB", $fileSizeBytes / 1073741824); + } elseif ($fileSizeBytes >= 1048576) { + $fileSizeFormatted = sprintf("%.1f MB", $fileSizeBytes / 1048576); + } elseif ($fileSizeBytes >= 1024) { + $fileSizeFormatted = sprintf("%.1f KB", $fileSizeBytes / 1024); + } else { + $fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes)); + } + + $fileEntry = [ + 'name' => $file, + 'modified' => $fileDateModified, + 'uploaded' => $fileUploadedDate, + 'size' => $fileSizeFormatted, + 'uploader' => $fileUploader, + 'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : [] + ]; + + // Optionally include file content for text-based files. + if (preg_match('/\.(txt|html|htm|md|js|css|json|xml|php|py|ini|conf|log)$/i', $file)) { + $content = file_get_contents($filePath); + $fileEntry['content'] = $content; + } + + $fileList[] = $fileEntry; + } + + // Load global tags. + $globalTagsFile = META_DIR . "createdTags.json"; + $globalTags = file_exists($globalTagsFile) ? json_decode(file_get_contents($globalTagsFile), true) : []; + + return ["files" => $fileList, "globalTags" => $globalTags]; + } +} \ No newline at end of file diff --git a/src/models/FolderModel.php b/src/models/FolderModel.php new file mode 100644 index 0000000..b532c18 --- /dev/null +++ b/src/models/FolderModel.php @@ -0,0 +1,570 @@ + "Invalid folder name."]; + } + if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) { + return ["error" => "Invalid parent folder name."]; + } + + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + if ($parent !== "" && strtolower($parent) !== "root") { + $fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName; + $relativePath = $parent . "/" . $folderName; + } else { + $fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName; + $relativePath = $folderName; + } + + // Check if the folder already exists. + if (file_exists($fullPath)) { + return ["error" => "Folder already exists."]; + } + + // Attempt to create the folder. + if (mkdir($fullPath, 0755, true)) { + // Create an empty metadata file for the new folder. + $metadataFile = self::getMetadataFilePath($relativePath); + if (file_put_contents($metadataFile, json_encode([], JSON_PRETTY_PRINT)) === false) { + return ["error" => "Folder created but failed to create metadata file."]; + } + return ["success" => true]; + } else { + return ["error" => "Failed to create folder."]; + } + } + + /** + * Generates the metadata file path for a given folder. + * + * @param string $folder The relative folder path. + * @return string The metadata file path. + */ + private static function getMetadataFilePath(string $folder): string { + if (strtolower($folder) === 'root' || trim($folder) === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; + } + + /** + * Deletes a folder if it is empty and removes its corresponding metadata. + * + * @param string $folder The folder name (relative to the upload directory). + * @return array An associative array with "success" on success or "error" on failure. + */ + public static function deleteFolder(string $folder): array { + // Prevent deletion of "root". + if (strtolower($folder) === 'root') { + return ["error" => "Cannot delete root folder."]; + } + + // Validate folder name. + if (!preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name."]; + } + + // Build the full folder path. + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; + + // Check if the folder exists and is a directory. + if (!file_exists($folderPath) || !is_dir($folderPath)) { + return ["error" => "Folder does not exist."]; + } + + // Prevent deletion if the folder is not empty. + $items = array_diff(scandir($folderPath), array('.', '..')); + if (count($items) > 0) { + return ["error" => "Folder is not empty."]; + } + + // Attempt to delete the folder. + if (rmdir($folderPath)) { + // Remove corresponding metadata file. + $metadataFile = self::getMetadataFilePath($folder); + if (file_exists($metadataFile)) { + unlink($metadataFile); + } + return ["success" => true]; + } else { + return ["error" => "Failed to delete folder."]; + } + } + + /** + * Renames a folder and updates related metadata files. + * + * @param string $oldFolder The current folder name (relative to UPLOAD_DIR). + * @param string $newFolder The new folder name. + * @return array Returns an associative array with "success" on success or "error" on failure. + */ + public static function renameFolder(string $oldFolder, string $newFolder): array { + // Sanitize and trim folder names. + $oldFolder = trim($oldFolder, "/\\ "); + $newFolder = trim($newFolder, "/\\ "); + + // Validate folder names. + if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) { + return ["error" => "Invalid folder name(s)."]; + } + + // Build the full folder paths. + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + $oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder; + $newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder; + + // Validate that the old folder exists and new folder does not. + if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) || + strpos(realpath($oldPath), realpath($baseDir)) !== 0 || + strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) { + return ["error" => "Invalid folder path."]; + } + + if (!file_exists($oldPath) || !is_dir($oldPath)) { + return ["error" => "Folder to rename does not exist."]; + } + + if (file_exists($newPath)) { + return ["error" => "New folder name already exists."]; + } + + // Attempt to rename the folder. + if (rename($oldPath, $newPath)) { + // Update metadata: Rename all metadata files that have the old folder prefix. + $oldPrefix = str_replace(['/', '\\', ' '], '-', $oldFolder); + $newPrefix = str_replace(['/', '\\', ' '], '-', $newFolder); + $metadataFiles = glob(META_DIR . $oldPrefix . '*_metadata.json'); + foreach ($metadataFiles as $oldMetaFile) { + $baseName = basename($oldMetaFile); + $newBaseName = preg_replace('/^' . preg_quote($oldPrefix, '/') . '/', $newPrefix, $baseName); + $newMetaFile = META_DIR . $newBaseName; + rename($oldMetaFile, $newMetaFile); + } + return ["success" => true]; + } else { + return ["error" => "Failed to rename folder."]; + } + } + + /** + * Recursively scans a directory for subfolders. + * + * @param string $dir The full path to the directory. + * @param string $relative The relative path from the base directory. + * @return array An array of folder paths (relative to the base). + */ + private static function getSubfolders(string $dir, string $relative = ''): array { + $folders = []; + $items = scandir($dir); + $safeFolderNamePattern = REGEX_FOLDER_NAME; + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + if (!preg_match($safeFolderNamePattern, $item)) { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $folderPath = ($relative ? $relative . '/' : '') . $item; + $folders[] = $folderPath; + $subFolders = self::getSubfolders($path, $folderPath); + $folders = array_merge($folders, $subFolders); + } + } + return $folders; + } + + /** + * Retrieves the list of folders (including "root") along with file count metadata. + * + * @return array An array of folder information arrays. + */ + public static function getFolderList(): array { + $baseDir = rtrim(UPLOAD_DIR, '/\\'); + $folderInfoList = []; + + // Process the "root" folder. + $rootMetaFile = self::getMetadataFilePath('root'); + $rootFileCount = 0; + if (file_exists($rootMetaFile)) { + $rootMetadata = json_decode(file_get_contents($rootMetaFile), true); + $rootFileCount = is_array($rootMetadata) ? count($rootMetadata) : 0; + } + $folderInfoList[] = [ + "folder" => "root", + "fileCount" => $rootFileCount, + "metadataFile" => basename($rootMetaFile) + ]; + + // Recursively scan for subfolders. + if (is_dir($baseDir)) { + $subfolders = self::getSubfolders($baseDir); + } else { + $subfolders = []; + } + + // For each subfolder, load metadata to get file counts. + foreach ($subfolders as $folder) { + $metaFile = self::getMetadataFilePath($folder); + $fileCount = 0; + if (file_exists($metaFile)) { + $metadata = json_decode(file_get_contents($metaFile), true); + $fileCount = is_array($metadata) ? count($metadata) : 0; + } + $folderInfoList[] = [ + "folder" => $folder, + "fileCount" => $fileCount, + "metadataFile" => basename($metaFile) + ]; + } + + return $folderInfoList; + } + + /** + * Retrieves the share folder record for a given token. + * + * @param string $token The share folder token. + * @return array|null The share folder record, or null if not found. + */ + public static function getShareFolderRecord(string $token): ?array { + $shareFile = META_DIR . "share_folder_links.json"; + if (!file_exists($shareFile)) { + return null; + } + $shareLinks = json_decode(file_get_contents($shareFile), true); + if (!is_array($shareLinks) || !isset($shareLinks[$token])) { + return null; + } + return $shareLinks[$token]; + } + + /** + * Retrieves shared folder data based on a share token. + * + * @param string $token The share folder token. + * @param string|null $providedPass The provided password (if any). + * @param int $page The page number for pagination. + * @param int $itemsPerPage The number of files to display per page. + * @return array Associative array with keys: + * - 'record': the share record, + * - 'folder': the shared folder (relative), + * - 'realFolderPath': absolute folder path, + * - 'files': array of filenames for the current page, + * - 'currentPage': current page number, + * - 'totalPages': total pages, + * or an 'error' key on failure. + */ + public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array { + // Load the share folder record. + $shareFile = META_DIR . "share_folder_links.json"; + if (!file_exists($shareFile)) { + return ["error" => "Share link not found."]; + } + $shareLinks = json_decode(file_get_contents($shareFile), true); + if (!is_array($shareLinks) || !isset($shareLinks[$token])) { + return ["error" => "Share link not found."]; + } + $record = $shareLinks[$token]; + // Check expiration. + if (time() > $record['expires']) { + return ["error" => "This share link has expired."]; + } + // If password protection is enabled and no password is provided, signal that. + if (!empty($record['password']) && empty($providedPass)) { + return ["needs_password" => true]; + } + if (!empty($record['password']) && !password_verify($providedPass, $record['password'])) { + return ["error" => "Invalid password."]; + } + // Determine the shared folder. + $folder = trim($record['folder'], "/\\ "); + $baseDir = realpath(UPLOAD_DIR); + if ($baseDir === false) { + return ["error" => "Uploads directory not configured correctly."]; + } + if (!empty($folder) && strtolower($folder) !== 'root') { + $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; + } else { + $folder = "root"; + $folderPath = $baseDir; + } + $realFolderPath = realpath($folderPath); + $uploadDirReal = realpath(UPLOAD_DIR); + if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { + return ["error" => "Shared folder not found."]; + } + // Scan for files (only files). + $allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) { + return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item); + })); + sort($allFiles); + $totalFiles = count($allFiles); + $totalPages = max(1, ceil($totalFiles / $itemsPerPage)); + $currentPage = min($page, $totalPages); + $startIndex = ($currentPage - 1) * $itemsPerPage; + $filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage); + + return [ + "record" => $record, + "folder" => $folder, + "realFolderPath" => $realFolderPath, + "files" => $filesOnPage, + "currentPage" => $currentPage, + "totalPages" => $totalPages + ]; + } + + /** + * Creates a share link for a folder. + * + * @param string $folder The folder to share (relative to UPLOAD_DIR). + * @param int $expirationMinutes The duration (in minutes) until the link expires. + * @param string $password Optional password for the share. + * @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed. + * @return array An associative array with "token", "expires", and "link" on success, or "error" on failure. + */ + public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array { + // Validate folder name. + if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name."]; + } + + // Generate secure token. + try { + $token = bin2hex(random_bytes(16)); // 32 hex characters. + } catch (Exception $e) { + return ["error" => "Could not generate token."]; + } + + // Calculate expiration time. + $expires = time() + ($expirationMinutes * 60); + + // Hash the password if provided. + $hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : ""; + + // Define the share folder links file. + $shareFile = META_DIR . "share_folder_links.json"; + $shareLinks = []; + if (file_exists($shareFile)) { + $data = file_get_contents($shareFile); + $shareLinks = json_decode($data, true); + if (!is_array($shareLinks)) { + $shareLinks = []; + } + } + + // Clean up expired share links. + $currentTime = time(); + foreach ($shareLinks as $key => $link) { + if (isset($link["expires"]) && $link["expires"] < $currentTime) { + unset($shareLinks[$key]); + } + } + + // Add new share record. + $shareLinks[$token] = [ + "folder" => $folder, + "expires" => $expires, + "password" => $hashedPassword, + "allowUpload" => $allowUpload + ]; + + // Save the updated share links. + if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) { + return ["error" => "Could not save share link."]; + } + + // Determine the base URL. + if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) { + $baseUrl = rtrim(BASE_URL, '/'); + } else { + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http"; + $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost'); + $baseUrl = $protocol . "://" . $host; + } + // The share URL points to the shared folder page. + $link = $baseUrl . "api/folder/shareFolder.php?token=" . urlencode($token); + + return ["token" => $token, "expires" => $expires, "link" => $link]; + } + + /** + * Retrieves information for a shared file from a shared folder link. + * + * @param string $token The share folder token. + * @param string $file The requested file name. + * @return array An associative array with keys: + * - "error": error message, if any, + * - "realFilePath": the absolute path to the file, + * - "mimeType": the detected MIME type. + */ + public static function getSharedFileInfo(string $token, string $file): array { + // Load the share folder record. + $shareFile = META_DIR . "share_folder_links.json"; + if (!file_exists($shareFile)) { + return ["error" => "Share link not found."]; + } + $shareLinks = json_decode(file_get_contents($shareFile), true); + if (!is_array($shareLinks) || !isset($shareLinks[$token])) { + return ["error" => "Share link not found."]; + } + $record = $shareLinks[$token]; + + // Check if the link has expired. + if (time() > $record['expires']) { + return ["error" => "This share link has expired."]; + } + + // Determine the shared folder. + $folder = trim($record['folder'], "/\\ "); + $baseDir = realpath(UPLOAD_DIR); + if ($baseDir === false) { + return ["error" => "Uploads directory not configured correctly."]; + } + if (!empty($folder) && strtolower($folder) !== 'root') { + $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; + } else { + $folderPath = $baseDir; + } + $realFolderPath = realpath($folderPath); + $uploadDirReal = realpath(UPLOAD_DIR); + if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { + return ["error" => "Shared folder not found."]; + } + + // Sanitize the file name to prevent path traversal. + if (strpos($file, "/") !== false || strpos($file, "\\") !== false) { + return ["error" => "Invalid file name."]; + } + $file = basename($file); + + // Build the full file path. + $filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file; + $realFilePath = realpath($filePath); + if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) { + return ["error" => "File not found."]; + } + + $mimeType = mime_content_type($realFilePath); + return [ + "realFilePath" => $realFilePath, + "mimeType" => $mimeType + ]; + } + + /** + * Handles uploading a file to a shared folder. + * + * @param string $token The share folder token. + * @param array $fileUpload The $_FILES['fileToUpload'] array. + * @return array An associative array with "success" on success or "error" on failure. + */ + public static function uploadToSharedFolder(string $token, array $fileUpload): array { + // Define maximum file size and allowed extensions. + $maxSize = 50 * 1024 * 1024; // 50 MB + $allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv']; + + // Load the share folder record. + $shareFile = META_DIR . "share_folder_links.json"; + if (!file_exists($shareFile)) { + return ["error" => "Share record not found."]; + } + $shareLinks = json_decode(file_get_contents($shareFile), true); + if (!is_array($shareLinks) || !isset($shareLinks[$token])) { + return ["error" => "Invalid share token."]; + } + $record = $shareLinks[$token]; + + // Check expiration. + if (time() > $record['expires']) { + return ["error" => "This share link has expired."]; + } + + // Check whether uploads are allowed. + if (empty($record['allowUpload']) || $record['allowUpload'] != 1) { + return ["error" => "File uploads are not allowed for this share."]; + } + + // Validate file upload presence. + if ($fileUpload['error'] !== UPLOAD_ERR_OK) { + return ["error" => "File upload error. Code: " . $fileUpload['error']]; + } + + if ($fileUpload['size'] > $maxSize) { + return ["error" => "File size exceeds allowed limit."]; + } + + $uploadedName = basename($fileUpload['name']); + $ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowedExtensions)) { + return ["error" => "File type not allowed."]; + } + + // Determine the target folder from the share record. + $folderName = trim($record['folder'], "/\\"); + $targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; + if (!empty($folderName) && strtolower($folderName) !== 'root') { + $targetFolder .= $folderName; + } + + // Verify target folder exists. + $realTargetFolder = realpath($targetFolder); + $uploadDirReal = realpath(UPLOAD_DIR); + if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) { + return ["error" => "Shared folder not found."]; + } + + // Generate a new filename (using uniqid and sanitizing the original name). + $newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName); + $targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename; + + // Move the uploaded file. + if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) { + return ["error" => "Failed to move the uploaded file."]; + } + + // --- Metadata Update --- + // Determine metadata file. + $metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName; + $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; + $metadataFile = META_DIR . $metadataFileName; + $metadataCollection = []; + if (file_exists($metadataFile)) { + $data = file_get_contents($metadataFile); + $metadataCollection = json_decode($data, true); + if (!is_array($metadataCollection)) { + $metadataCollection = []; + } + } + $uploadedDate = date(DATE_TIME_FORMAT); + $uploader = "Outside Share"; // As per your original implementation. + // Update metadata with the new file's info. + $metadataCollection[$newFilename] = [ + "uploaded" => $uploadedDate, + "uploader" => $uploader + ]; + file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT)); + + return ["success" => "File uploaded successfully.", "newFilename" => $newFilename]; + } +} \ No newline at end of file diff --git a/src/models/UploadModel.php b/src/models/UploadModel.php new file mode 100644 index 0000000..358ea70 --- /dev/null +++ b/src/models/UploadModel.php @@ -0,0 +1,266 @@ + file_exists($chunkFile) ? "found" : "not found"]; + } + + // Handle chunked uploads. + if (isset($post['resumableChunkNumber'])) { + $chunkNumber = intval($post['resumableChunkNumber']); + $totalChunks = intval($post['resumableTotalChunks']); + $resumableIdentifier = $post['resumableIdentifier'] ?? ''; + $resumableFilename = urldecode(basename($post['resumableFilename'])); + + // Validate file name. + if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { + return ["error" => "Invalid file name: $resumableFilename"]; + } + + $folder = isset($post['folder']) ? trim($post['folder']) : 'root'; + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name"]; + } + + $baseUploadDir = UPLOAD_DIR; + if ($folder !== 'root') { + $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; + } + if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { + return ["error" => "Failed to create upload directory"]; + } + + $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; + if (!is_dir($tempDir) && !mkdir($tempDir, 0775, true)) { + return ["error" => "Failed to create temporary chunk directory"]; + } + + if (!isset($files["file"]) || $files["file"]["error"] !== UPLOAD_ERR_OK) { + return ["error" => "Upload error on chunk $chunkNumber"]; + } + + $chunkFile = $tempDir . $chunkNumber; + if (!move_uploaded_file($files["file"]["tmp_name"], $chunkFile)) { + return ["error" => "Failed to move uploaded chunk $chunkNumber"]; + } + + // Check if all chunks are present. + $allChunksPresent = true; + for ($i = 1; $i <= $totalChunks; $i++) { + if (!file_exists($tempDir . $i)) { + $allChunksPresent = false; + break; + } + } + if (!$allChunksPresent) { + return ["status" => "chunk uploaded"]; + } + + // Merge chunks. + $targetPath = $baseUploadDir . $resumableFilename; + if (!$out = fopen($targetPath, "wb")) { + return ["error" => "Failed to open target file for writing"]; + } + for ($i = 1; $i <= $totalChunks; $i++) { + $chunkPath = $tempDir . $i; + if (!file_exists($chunkPath)) { + fclose($out); + return ["error" => "Chunk $i missing during merge"]; + } + if (!$in = fopen($chunkPath, "rb")) { + fclose($out); + return ["error" => "Failed to open chunk $i"]; + } + while ($buff = fread($in, 4096)) { + fwrite($out, $buff); + } + fclose($in); + } + fclose($out); + + // Update metadata. + $relativeFolder = $folder; + $metadataKey = ($relativeFolder === '' || strtolower($relativeFolder) === 'root') ? "root" : $relativeFolder; + $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; + $metadataFile = META_DIR . $metadataFileName; + $uploadedDate = date(DATE_TIME_FORMAT); + $uploader = $_SESSION['username'] ?? "Unknown"; + $metadataCollection = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + if (!is_array($metadataCollection)) { + $metadataCollection = []; + } + if (!isset($metadataCollection[$resumableFilename])) { + $metadataCollection[$resumableFilename] = [ + "uploaded" => $uploadedDate, + "uploader" => $uploader + ]; + file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT)); + } + + // Cleanup temporary directory. + $rrmdir = function($dir) use (&$rrmdir) { + if (!is_dir($dir)) return; + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($iterator as $item) { + $item->isDir() ? rmdir($item->getRealPath()) : unlink($item->getRealPath()); + } + rmdir($dir); + }; + $rrmdir($tempDir); + + return ["success" => "File uploaded successfully"]; + } else { + // Handle full upload (non-chunked). + $folder = isset($post['folder']) ? trim($post['folder']) : 'root'; + if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + return ["error" => "Invalid folder name"]; + } + + $baseUploadDir = UPLOAD_DIR; + if ($folder !== 'root') { + $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; + } + if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { + return ["error" => "Failed to create upload directory"]; + } + + $safeFileNamePattern = REGEX_FILE_NAME; + $metadataCollection = []; + $metadataChanged = []; + + foreach ($files["file"]["name"] as $index => $fileName) { + $safeFileName = trim(urldecode(basename($fileName))); + if (!preg_match($safeFileNamePattern, $safeFileName)) { + return ["error" => "Invalid file name: " . $fileName]; + } + $relativePath = ''; + if (isset($post['relativePath'])) { + $relativePath = is_array($post['relativePath']) ? $post['relativePath'][$index] ?? '' : $post['relativePath']; + } + $uploadDir = $baseUploadDir; + if (!empty($relativePath)) { + $subDir = dirname($relativePath); + if ($subDir !== '.' && $subDir !== '') { + $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $subDir) . DIRECTORY_SEPARATOR; + } + $safeFileName = basename($relativePath); + } + if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) { + return ["error" => "Failed to create subfolder"]; + } + $targetPath = $uploadDir . $safeFileName; + if (move_uploaded_file($files["file"]["tmp_name"][$index], $targetPath)) { + $folderPath = $folder; + $metadataKey = ($folderPath === '' || strtolower($folderPath) === 'root') ? "root" : $folderPath; + $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; + $metadataFile = META_DIR . $metadataFileName; + if (!isset($metadataCollection[$metadataKey])) { + $metadataCollection[$metadataKey] = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + if (!is_array($metadataCollection[$metadataKey])) { + $metadataCollection[$metadataKey] = []; + } + $metadataChanged[$metadataKey] = false; + } + if (!isset($metadataCollection[$metadataKey][$safeFileName])) { + $uploadedDate = date(DATE_TIME_FORMAT); + $uploader = $_SESSION['username'] ?? "Unknown"; + $metadataCollection[$metadataKey][$safeFileName] = [ + "uploaded" => $uploadedDate, + "uploader" => $uploader + ]; + $metadataChanged[$metadataKey] = true; + } + } else { + return ["error" => "Error uploading file"]; + } + } + + foreach ($metadataCollection as $folderKey => $data) { + if ($metadataChanged[$folderKey]) { + $metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json'; + $metadataFile = META_DIR . $metadataFileName; + file_put_contents($metadataFile, json_encode($data, JSON_PRETTY_PRINT)); + } + } + return ["success" => "Files uploaded successfully"]; + } + } + + /** + * Recursively removes a directory and its contents. + * + * @param string $dir The directory to remove. + * @return void + */ + private static function rrmdir(string $dir): void { + if (!is_dir($dir)) { + return; + } + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($iterator as $file) { + if ($file->isDir()) { + rmdir($file->getRealPath()); + } else { + unlink($file->getRealPath()); + } + } + rmdir($dir); + } + + /** + * Removes the temporary chunk directory for resumable uploads. + * + * The folder name is expected to exactly match the "resumable_" pattern. + * + * @param string $folder The folder name provided (URL-decoded). + * @return array Returns a status array indicating success or error. + */ + public static function removeChunks(string $folder): array { + $folder = urldecode($folder); + // The folder name should exactly match the "resumable_" pattern. + $regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; + if (!preg_match($regex, $folder)) { + return ["error" => "Invalid folder name"]; + } + + $tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; + if (!is_dir($tempDir)) { + return ["success" => true, "message" => "Temporary folder already removed."]; + } + + self::rrmdir($tempDir); + + if (!is_dir($tempDir)) { + return ["success" => true, "message" => "Temporary folder removed."]; + } else { + return ["error" => "Failed to remove temporary folder."]; + } + } +} \ No newline at end of file diff --git a/src/models/UserModel.php b/src/models/UserModel.php new file mode 100644 index 0000000..ac34033 --- /dev/null +++ b/src/models/UserModel.php @@ -0,0 +1,657 @@ += 3 && preg_match(REGEX_USER, $parts[0])) { + $users[] = [ + "username" => $parts[0], + "role" => trim($parts[2]) + ]; + } + } + } + return $users; + } + + /** + * Adds a new user. + * + * @param string $username The new username. + * @param string $password The plain-text password. + * @param string $isAdmin "1" if admin; "0" otherwise. + * @param bool $setupMode If true, overwrite the users file. + * @return array Response containing either an error or a success message. + */ + public static function addUser($username, $password, $isAdmin, $setupMode) { + $usersFile = USERS_DIR . USERS_FILE; + + // Ensure users.txt exists. + if (!file_exists($usersFile)) { + file_put_contents($usersFile, ''); + } + + // Check if username already exists. + $existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($existingUsers as $line) { + $parts = explode(':', trim($line)); + if ($username === $parts[0]) { + return ["error" => "User already exists"]; + } + } + + // Hash the password. + $hashedPassword = password_hash($password, PASSWORD_BCRYPT); + + // Prepare the new line. + $newUserLine = $username . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL; + + // If setup mode, overwrite the file; otherwise, append. + if ($setupMode) { + file_put_contents($usersFile, $newUserLine); + } else { + file_put_contents($usersFile, $newUserLine, FILE_APPEND); + } + + return ["success" => "User added successfully"]; + } + + /** + * Removes the specified user from the users file and updates the userPermissions file. + * + * @param string $usernameToRemove The username to remove. + * @return array An array with either an error message or a success message. + */ + public static function removeUser($usernameToRemove) { + $usersFile = USERS_DIR . USERS_FILE; + + if (!file_exists($usersFile)) { + return ["error" => "Users file not found"]; + } + + $existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $newUsers = []; + $userFound = false; + + // Loop through users; skip (remove) the specified user. + foreach ($existingUsers as $line) { + $parts = explode(':', trim($line)); + if (count($parts) < 3) { + continue; + } + if ($parts[0] === $usernameToRemove) { + $userFound = true; + continue; // Do not add this user to the new array. + } + $newUsers[] = $line; + } + + if (!$userFound) { + return ["error" => "User not found"]; + } + + // Write the updated user list back to the file. + file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL); + + // Update the userPermissions.json file. + $permissionsFile = USERS_DIR . "userPermissions.json"; + if (file_exists($permissionsFile)) { + $permissionsJson = file_get_contents($permissionsFile); + $permissionsArray = json_decode($permissionsJson, true); + if (is_array($permissionsArray) && isset($permissionsArray[$usernameToRemove])) { + unset($permissionsArray[$usernameToRemove]); + file_put_contents($permissionsFile, json_encode($permissionsArray, JSON_PRETTY_PRINT)); + } + } + + return ["success" => "User removed successfully"]; + } + + /** + * Retrieves permissions from the userPermissions.json file. + * If the current user is an admin, returns all permissions. + * Otherwise, returns only the permissions for the current user. + * + * @return array|object Returns an associative array of permissions or an empty object if none are found. + */ + public static function getUserPermissions() { + global $encryptionKey; + $permissionsFile = USERS_DIR . "userPermissions.json"; + $permissionsArray = []; + + // Load permissions if the file exists. + if (file_exists($permissionsFile)) { + $content = file_get_contents($permissionsFile); + // Attempt to decrypt the content. + $decryptedContent = decryptData($content, $encryptionKey); + if ($decryptedContent === false) { + // If decryption fails, assume the content is plain JSON. + $permissionsArray = json_decode($content, true); + } else { + $permissionsArray = json_decode($decryptedContent, true); + } + if (!is_array($permissionsArray)) { + $permissionsArray = []; + } + } + + // If the user is an admin, return all permissions. + if (isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true) { + return $permissionsArray; + } + + // Otherwise, return only the permissions for the currently logged-in user. + $username = $_SESSION['username'] ?? ''; + foreach ($permissionsArray as $storedUsername => $data) { + if (strcasecmp($storedUsername, $username) === 0) { + return $data; + } + } + + // If no permissions are found, return an empty object. + return new stdClass(); + } + + /** + * Updates user permissions in the userPermissions.json file. + * + * @param array $permissions An array of permission updates. + * @return array An associative array with a success or error message. + */ + public static function updateUserPermissions($permissions) { + global $encryptionKey; + $permissionsFile = USERS_DIR . "userPermissions.json"; + $existingPermissions = []; + + // Load existing permissions if available and decrypt. + if (file_exists($permissionsFile)) { + $encryptedContent = file_get_contents($permissionsFile); + $json = decryptData($encryptedContent, $encryptionKey); + $existingPermissions = json_decode($json, true); + if (!is_array($existingPermissions)) { + $existingPermissions = []; + } + } + + // Load user roles from the users file. + $usersFile = USERS_DIR . USERS_FILE; + $userRoles = []; + if (file_exists($usersFile)) { + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 3 && preg_match(REGEX_USER, $parts[0])) { + // Use lowercase keys for consistency. + $userRoles[strtolower($parts[0])] = trim($parts[2]); + } + } + } + + // Process each permission update. + foreach ($permissions as $perm) { + if (!isset($perm['username'])) { + continue; + } + $username = $perm['username']; + // Look up the user's role. + $role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null; + + // Skip updating permissions for admin users. + if ($role === "1") { + continue; + } + + // Update permissions: default any missing value to false. + $existingPermissions[strtolower($username)] = [ + 'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false, + 'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false, + 'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false + ]; + } + + // Convert the updated permissions array to JSON. + $plainText = json_encode($existingPermissions, JSON_PRETTY_PRINT); + // Encrypt the JSON. + $encryptedData = encryptData($plainText, $encryptionKey); + // Save encrypted permissions back to the file. + $result = file_put_contents($permissionsFile, $encryptedData); + if ($result === false) { + return ["error" => "Failed to save user permissions."]; + } + + return ["success" => "User permissions updated successfully."]; + } + + /** + * Changes the password for the given user. + * + * @param string $username The username whose password is to be changed. + * @param string $oldPassword The old (current) password. + * @param string $newPassword The new password. + * @return array An array with either a success or error message. + */ + public static function changePassword($username, $oldPassword, $newPassword) { + $usersFile = USERS_DIR . USERS_FILE; + + if (!file_exists($usersFile)) { + return ["error" => "Users file not found"]; + } + + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $userFound = false; + $newLines = []; + + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + // Expect at least 3 parts: username, hashed password, and role. + if (count($parts) < 3) { + $newLines[] = $line; + continue; + } + $storedUser = $parts[0]; + $storedHash = $parts[1]; + $storedRole = $parts[2]; + // Preserve TOTP secret if it exists. + $totpSecret = (count($parts) >= 4) ? $parts[3] : ""; + + if ($storedUser === $username) { + $userFound = true; + // Verify the old password. + if (!password_verify($oldPassword, $storedHash)) { + return ["error" => "Old password is incorrect."]; + } + // Hash the new password. + $newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); + + // Rebuild the line, preserving TOTP secret if it exists. + if ($totpSecret !== "") { + $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret; + } else { + $newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole; + } + } else { + $newLines[] = $line; + } + } + + if (!$userFound) { + return ["error" => "User not found."]; + } + + // Save the updated users file. + if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) { + return ["success" => "Password updated successfully."]; + } else { + return ["error" => "Could not update password."]; + } + } + + /** + * Updates the user panel settings by disabling the TOTP secret if TOTP is not enabled. + * + * @param string $username The username whose panel settings are being updated. + * @param bool $totp_enabled Whether TOTP is enabled. + * @return array An array indicating success or failure. + */ + public static function updateUserPanel($username, $totp_enabled) { + $usersFile = USERS_DIR . USERS_FILE; + + if (!file_exists($usersFile)) { + return ["error" => "Users file not found"]; + } + + // If TOTP is disabled, update the file to clear the TOTP secret. + if (!$totp_enabled) { + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $newLines = []; + + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + // Leave lines with fewer than three parts unchanged. + if (count($parts) < 3) { + $newLines[] = $line; + continue; + } + + if ($parts[0] === $username) { + // If a fourth field (TOTP secret) exists, clear it; otherwise, append an empty field. + if (count($parts) >= 4) { + $parts[3] = ""; + } else { + $parts[] = ""; + } + $newLines[] = implode(':', $parts); + } else { + $newLines[] = $line; + } + } + + $result = file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); + if ($result === false) { + return ["error" => "Failed to disable TOTP secret"]; + } + return ["success" => "User panel updated: TOTP disabled"]; + } + + // If TOTP is enabled, do nothing. + return ["success" => "User panel updated: TOTP remains enabled"]; + } + + /** + * Disables the TOTP secret for the specified user. + * + * @param string $username The user for whom TOTP should be disabled. + * @return bool True if the secret was cleared; false otherwise. + */ + public static function disableTOTPSecret($username) { + global $encryptionKey; // In case it's used in this model context. + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) { + return false; + } + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $modified = false; + $newLines = []; + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + // If the line doesn't have at least three parts, leave it unchanged. + if (count($parts) < 3) { + $newLines[] = $line; + continue; + } + if ($parts[0] === $username) { + // If a fourth field exists, clear it; otherwise, append an empty field. + if (count($parts) >= 4) { + $parts[3] = ""; + } else { + $parts[] = ""; + } + $modified = true; + $newLines[] = implode(":", $parts); + } else { + $newLines[] = $line; + } + } + if ($modified) { + file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); + } + return $modified; + } + + /** + * Attempts to recover TOTP for a user using the supplied recovery code. + * + * @param string $userId The user identifier. + * @param string $recoveryCode The recovery code provided by the user. + * @return array An associative array with keys 'status' and 'message'. + */ + public static function recoverTOTP($userId, $recoveryCode) { + // --- Rate‑limit recovery attempts --- + $attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json'; + $attempts = is_file($attemptsFile) ? json_decode(file_get_contents($attemptsFile), true) : []; + $key = $_SERVER['REMOTE_ADDR'] . '|' . $userId; + $now = time(); + if (isset($attempts[$key])) { + // Prune attempts older than 15 minutes. + $attempts[$key] = array_filter($attempts[$key], function($ts) use ($now) { + return $ts > $now - 900; + }); + } + if (count($attempts[$key] ?? []) >= 5) { + return ['status' => 'error', 'message' => 'Too many attempts. Try again later.']; + } + + // --- Load user metadata file --- + $userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json'; + if (!file_exists($userFile)) { + return ['status' => 'error', 'message' => 'User not found']; + } + + // --- Open and lock file --- + $fp = fopen($userFile, 'c+'); + if (!$fp || !flock($fp, LOCK_EX)) { + return ['status' => 'error', 'message' => 'Server error']; + } + + $fileContents = stream_get_contents($fp); + $data = json_decode($fileContents, true) ?: []; + + // --- Check recovery code --- + if (empty($recoveryCode)) { + flock($fp, LOCK_UN); + fclose($fp); + return ['status' => 'error', 'message' => 'Recovery code required']; + } + + $storedHash = $data['totp_recovery_code'] ?? null; + if (!$storedHash || !password_verify($recoveryCode, $storedHash)) { + // Record failed attempt. + $attempts[$key][] = $now; + file_put_contents($attemptsFile, json_encode($attempts), LOCK_EX); + flock($fp, LOCK_UN); + fclose($fp); + return ['status' => 'error', 'message' => 'Invalid recovery code']; + } + + // --- Invalidate recovery code --- + $data['totp_recovery_code'] = null; + rewind($fp); + ftruncate($fp, 0); + fwrite($fp, json_encode($data)); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + + return ['status' => 'ok']; + } + + /** + * Generates a random recovery code. + * + * @param int $length Length of the recovery code. + * @return string + */ + private static function generateRecoveryCode($length = 12) { + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $max = strlen($chars) - 1; + $code = ''; + for ($i = 0; $i < $length; $i++) { + $code .= $chars[random_int(0, $max)]; + } + return $code; + } + + /** + * Saves a new TOTP recovery code for the specified user. + * + * @param string $userId The username of the user. + * @return array An associative array with the status and recovery code (if successful). + */ + public static function saveTOTPRecoveryCode($userId) { + // Determine the user file path. + $userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json'; + + // Ensure the file exists; if not, create it with default data. + if (!file_exists($userFile)) { + $defaultData = []; + if (file_put_contents($userFile, json_encode($defaultData)) === false) { + return ['status' => 'error', 'message' => 'Server error: could not create user file']; + } + } + + // Generate a new recovery code. + $recoveryCode = self::generateRecoveryCode(); + $recoveryHash = password_hash($recoveryCode, PASSWORD_DEFAULT); + + // Open the file, lock it, and update the totp_recovery_code field. + $fp = fopen($userFile, 'c+'); + if (!$fp || !flock($fp, LOCK_EX)) { + return ['status' => 'error', 'message' => 'Server error: could not lock user file']; + } + + // Read and decode the existing JSON. + $contents = stream_get_contents($fp); + $data = json_decode($contents, true) ?: []; + + // Update the totp_recovery_code field. + $data['totp_recovery_code'] = $recoveryHash; + + // Write the new data. + rewind($fp); + ftruncate($fp, 0); + fwrite($fp, json_encode($data)); // Plain JSON in production. + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + + return ['status' => 'ok', 'recoveryCode' => $recoveryCode]; + } + + /** + * Sets up TOTP for the specified user by retrieving or generating a TOTP secret, + * then builds and returns a QR code image for the OTPAuth URL. + * + * @param string $username The username for which to set up TOTP. + * @return array An associative array with keys 'imageData' and 'mimeType', or 'error'. + */ + public static function setupTOTP($username) { + global $encryptionKey; + $usersFile = USERS_DIR . USERS_FILE; + + if (!file_exists($usersFile)) { + return ['error' => 'Users file not found']; + } + + // Look for an existing TOTP secret. + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $totpSecret = null; + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + $totpSecret = decryptData($parts[3], $encryptionKey); + break; + } + } + + // Use the TwoFactorAuth library to create a new secret if none found. + $tfa = new \RobThree\Auth\TwoFactorAuth( + new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), // QR code provider + 'FileRise', // issuer + 6, // number of digits + 30, // period (seconds) + \RobThree\Auth\Algorithm::Sha1 // algorithm + ); + if (!$totpSecret) { + $totpSecret = $tfa->createSecret(); + $encryptedSecret = encryptData($totpSecret, $encryptionKey); + + // Update the user’s line with the new encrypted secret. + $newLines = []; + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 3 && $parts[0] === $username) { + if (count($parts) >= 4) { + $parts[3] = $encryptedSecret; + } else { + $parts[] = $encryptedSecret; + } + $newLines[] = implode(':', $parts); + } else { + $newLines[] = $line; + } + } + file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); + } + + // Determine the OTPAuth URL. + // Try to load a global OTPAuth URL template from admin configuration. + $adminConfigFile = USERS_DIR . 'adminConfig.json'; + $globalOtpauthUrl = ""; + if (file_exists($adminConfigFile)) { + $encryptedContent = file_get_contents($adminConfigFile); + $decryptedContent = decryptData($encryptedContent, $encryptionKey); + if ($decryptedContent !== false) { + $config = json_decode($decryptedContent, true); + if (isset($config['globalOtpauthUrl']) && !empty($config['globalOtpauthUrl'])) { + $globalOtpauthUrl = $config['globalOtpauthUrl']; + } + } + } + + if (!empty($globalOtpauthUrl)) { + $label = "FileRise:" . $username; + $otpauthUrl = str_replace(["{label}", "{secret}"], [urlencode($label), $totpSecret], $globalOtpauthUrl); + } else { + $label = urlencode("FileRise:" . $username); + $issuer = urlencode("FileRise"); + $otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}"; + } + + // Build the QR code image using the Endroid QR Code Builder. + $result = \Endroid\QrCode\Builder\Builder::create() + ->writer(new \Endroid\QrCode\Writer\PngWriter()) + ->data($otpauthUrl) + ->build(); + + return [ + 'imageData' => $result->getString(), + 'mimeType' => $result->getMimeType() + ]; + } + + /** + * Retrieves the decrypted TOTP secret for a given user. + * + * @param string $username + * @return string|null Returns the TOTP secret if found, or null if not. + */ + public static function getTOTPSecret($username) { + global $encryptionKey; + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) { + return null; + } + $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $parts = explode(':', trim($line)); + // Expect at least 4 parts: username, hash, role, and TOTP secret. + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + return decryptData($parts[3], $encryptionKey); + } + } + return null; + } + + /** + * Helper to get a user's role from users.txt. + * + * @param string $username + * @return string|null + */ + public static function getUserRole($username) { + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) { + return null; + } + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 3 && $parts[0] === $username) { + return trim($parts[2]); + } + } + return null; + } +} \ No newline at end of file diff --git a/token.php b/token.php deleted file mode 100644 index 7ab4d4d..0000000 --- a/token.php +++ /dev/null @@ -1,8 +0,0 @@ - $_SESSION['csrf_token'], - "share_url" => SHARE_URL -]); -?> \ No newline at end of file diff --git a/totp_disable.php b/totp_disable.php deleted file mode 100644 index e648ecc..0000000 --- a/totp_disable.php +++ /dev/null @@ -1,74 +0,0 @@ - "Not authenticated"]); - exit; -} - -// Verify CSRF token from request headers. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - respond('error', 403, 'Invalid CSRF token'); -} - -header('Content-Type: application/json'); - -$username = $_SESSION['username'] ?? ''; -if (empty($username)) { - http_response_code(400); - echo json_encode(["error" => "Username not found in session"]); - exit; -} - -/** - * Removes the TOTP secret for the given user in users.txt. - * - * @param string $username - * @return bool True on success, false otherwise. - */ -function removeUserTOTPSecret($username) { - global $encryptionKey; - $usersFile = USERS_DIR . USERS_FILE; - if (!file_exists($usersFile)) { - return false; - } - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - $modified = false; - $newLines = []; - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) < 3) { - $newLines[] = $line; - continue; - } - if ($parts[0] === $username) { - // Remove the TOTP secret by setting it to an empty string. - if (count($parts) >= 4) { - $parts[3] = ""; - } - $modified = true; - $newLines[] = implode(":", $parts); - } else { - $newLines[] = $line; - } - } - if ($modified) { - file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); - } - return $modified; -} - -if (removeUserTOTPSecret($username)) { - echo json_encode(["success" => true, "message" => "TOTP disabled successfully."]); -} else { - http_response_code(500); - echo json_encode(["error" => "Failed to disable TOTP."]); -} -?> \ No newline at end of file diff --git a/totp_recover.php b/totp_recover.php deleted file mode 100644 index 4a9bd05..0000000 --- a/totp_recover.php +++ /dev/null @@ -1,115 +0,0 @@ -'error','message'=>'Method not allowed'])); -} - -// ——— 2) CSRF check ——— -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - respond('error', 403, 'Invalid CSRF token'); -} - -// ——— 3) Identify user to recover ——— -$userId = $_SESSION['username'] - ?? $_SESSION['pending_login_user'] - ?? null; - -if (!$userId) { - http_response_code(401); - error_log("Unauthorized recovery attempt from IP {$_SERVER['REMOTE_ADDR']}"); - exit(json_encode(['status'=>'error','message'=>'Unauthorized'])); -} - -// ——— Validate userId format ——— -if (!preg_match(REGEX_USER, $userId)) { - http_response_code(400); - error_log("Invalid userId format: {$userId}"); - exit(json_encode(['status'=>'error','message'=>'Invalid user identifier'])); -} - -// ——— Rate‑limit recovery attempts ——— -$attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json'; -$attempts = is_file($attemptsFile) - ? json_decode(file_get_contents($attemptsFile), true) - : []; -$key = $_SERVER['REMOTE_ADDR'] . '|' . $userId; -$now = time(); -// Prune >15 min old -if (isset($attempts[$key])) { - $attempts[$key] = array_filter( - $attempts[$key], - fn($ts) => $ts > $now - 900 - ); -} -if (count($attempts[$key] ?? []) >= 5) { - http_response_code(429); - exit(json_encode(['status'=>'error','message'=>'Too many attempts. Try again later.'])); -} - -// ——— 4) Load user metadata file ——— -$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json'; -if (!file_exists($userFile)) { - http_response_code(404); - error_log("User file not found for recovery: {$userFile}"); - exit(json_encode(['status'=>'error','message'=>'User not found'])); -} - -// ——— 5) Read & lock file ——— -$fp = fopen($userFile, 'c+'); -if (!$fp || !flock($fp, LOCK_EX)) { - http_response_code(500); - error_log("Failed to lock user file: {$userFile}"); - exit(json_encode(['status'=>'error','message'=>'Server error'])); -} -$data = json_decode(stream_get_contents($fp), true) ?: []; - -// ——— 6) Verify recovery code ——— -$input = json_decode(file_get_contents('php://input'), true)['recovery_code'] ?? ''; -if (!$input) { - flock($fp, LOCK_UN); - fclose($fp); - http_response_code(400); - exit(json_encode(['status'=>'error','message'=>'Recovery code required'])); -} - -$hash = $data['totp_recovery_code'] ?? null; -if (!$hash || !password_verify($input, $hash)) { - // record failed attempt - $attempts[$key][] = $now; - file_put_contents($attemptsFile, json_encode($attempts), LOCK_EX); - - flock($fp, LOCK_UN); - fclose($fp); - error_log("Invalid recovery code for user {$userId} from IP {$_SERVER['REMOTE_ADDR']}"); - exit(json_encode(['status'=>'error','message'=>'Invalid recovery code'])); -} - -// ——— 7) Invalidate code & save ——— -$data['totp_recovery_code'] = null; -rewind($fp); -ftruncate($fp, 0); -fwrite($fp, json_encode($data)); // no pretty-print in prod -fflush($fp); -flock($fp, LOCK_UN); -fclose($fp); - -// ——— 8) Finalize login ——— -session_regenerate_id(true); -$_SESSION['authenticated'] = true; -$_SESSION['username'] = $userId; -unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']); - -// ——— 9) Success ——— -echo json_encode(['status'=>'ok']); -exit; \ No newline at end of file diff --git a/totp_saveCode.php b/totp_saveCode.php deleted file mode 100644 index e2058a9..0000000 --- a/totp_saveCode.php +++ /dev/null @@ -1,85 +0,0 @@ -'error','message'=>'Method not allowed'])); -} - -// 2) CSRF check -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - respond('error', 403, 'Invalid CSRF token'); -} - -// 3) Must be logged in -if (empty($_SESSION['username'])) { - http_response_code(401); - error_log("totp_saveCode: unauthorized attempt from IP {$_SERVER['REMOTE_ADDR']}"); - exit(json_encode(['status'=>'error','message'=>'Unauthorized'])); -} - -// 4) Validate username format -$userId = $_SESSION['username']; -if (!preg_match(REGEX_USER, $userId)) { - http_response_code(400); - error_log("totp_saveCode: invalid username format: {$userId}"); - exit(json_encode(['status'=>'error','message'=>'Invalid user identifier'])); -} - -// 5) Ensure user file exists (create if missing) -$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json'; -if (!file_exists($userFile)) { - $defaultData = []; - if (file_put_contents($userFile, json_encode($defaultData)) === false) { - http_response_code(500); - error_log("totp_saveCode: failed to create user file: {$userFile}"); - exit(json_encode(['status'=>'error','message'=>'Server error'])); - } -} - -// 6) Generate secure recovery code -function generateRecoveryCode($length = 12) { - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $max = strlen($chars) - 1; - $code = ''; - for ($i = 0; $i < $length; $i++) { - $code .= $chars[random_int(0, $max)]; - } - return $code; -} -$recoveryCode = generateRecoveryCode(); -$recoveryHash = password_hash($recoveryCode, PASSWORD_DEFAULT); - -// 7) Read, lock, update user file -$fp = fopen($userFile, 'c+'); -if (!$fp || !flock($fp, LOCK_EX)) { - http_response_code(500); - error_log("totp_saveCode: failed to lock user file: {$userFile}"); - exit(json_encode(['status'=>'error','message'=>'Server error'])); -} - -$data = json_decode(stream_get_contents($fp), true) ?: []; -$data['totp_recovery_code'] = $recoveryHash; - -rewind($fp); -ftruncate($fp, 0); -fwrite($fp, json_encode($data)); // no pretty-print in prod -fflush($fp); -flock($fp, LOCK_UN); -fclose($fp); - -// 8) Return one-time recovery code -echo json_encode([ - 'status' => 'ok', - 'recoveryCode' => $recoveryCode -]); -exit; \ No newline at end of file diff --git a/totp_setup.php b/totp_setup.php deleted file mode 100644 index e4cf80d..0000000 --- a/totp_setup.php +++ /dev/null @@ -1,167 +0,0 @@ - $status, - 'code' => $code, - 'message' => $message, - 'data' => $data - ]); - exit; - } -} - -// Allow access if the user is authenticated or pending TOTP. -if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) { - http_response_code(403); - exit; -} - -// Retrieve CSRF token from GET parameter or request headers. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(["error" => "Invalid CSRF token"]); - http_response_code(403); - exit; -} - -$username = $_SESSION['username'] ?? ''; -if (!$username) { - http_response_code(400); - exit; -} - -// Set header to output a PNG image. -header("Content-Type: image/png"); - -// Define the path to your users.txt file. -$usersFile = USERS_DIR . USERS_FILE; - -/** - * Updates the TOTP secret for the given user in users.txt. - * - * @param string $username - * @param string $encryptedSecret The encrypted TOTP secret. - */ -function updateUserTOTPSecret($username, $encryptedSecret) { - global $usersFile; - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - $newLines = []; - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) < 3) { - $newLines[] = $line; - continue; - } - if ($parts[0] === $username) { - // If a fourth field exists, update it; otherwise, append it. - if (count($parts) >= 4) { - $parts[3] = $encryptedSecret; - } else { - $parts[] = $encryptedSecret; - } - $newLines[] = implode(':', $parts); - } else { - $newLines[] = $line; - } - } - file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); -} - -/** - * Retrieves the current user's TOTP secret from users.txt (if present). - * - * @param string $username - * @return string|null The decrypted TOTP secret or null if not found. - */ -function getUserTOTPSecret($username) { - global $usersFile, $encryptionKey; - if (!file_exists($usersFile)) { - return null; - } - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { - return decryptData($parts[3], $encryptionKey); - } - } - return null; -} - -/** - * Retrieves the global OTPAuth URL from admin configuration. - * - * @return string Global OTPAuth URL template or an empty string if not set. - */ -function getGlobalOtpauthUrl() { - global $encryptionKey; - $adminConfigFile = USERS_DIR . 'adminConfig.json'; - if (file_exists($adminConfigFile)) { - $encryptedContent = file_get_contents($adminConfigFile); - $decryptedContent = decryptData($encryptedContent, $encryptionKey); - if ($decryptedContent !== false) { - $config = json_decode($decryptedContent, true); - if (isset($config['globalOtpauthUrl']) && !empty($config['globalOtpauthUrl'])) { - return $config['globalOtpauthUrl']; - } - } - } - return ""; -} - -$tfa = new \RobThree\Auth\TwoFactorAuth( - new GoogleChartsQrCodeProvider(), // QR code provider - 'FileRise', // issuer - 6, // number of digits - 30, // period in seconds - Algorithm::Sha1 // enum case from your Algorithm enum -); - -// Retrieve the current TOTP secret for the user. -$totpSecret = getUserTOTPSecret($username); -if (!$totpSecret) { - // If no TOTP secret exists, generate a new one. - $totpSecret = $tfa->createSecret(); - $encryptedSecret = encryptData($totpSecret, $encryptionKey); - updateUserTOTPSecret($username, $encryptedSecret); -} - -// Determine the otpauth URL to use. -$globalOtpauthUrl = getGlobalOtpauthUrl(); -if (!empty($globalOtpauthUrl)) { - $label = "FileRise:" . $username; - $otpauthUrl = str_replace( - ["{label}", "{secret}"], - [urlencode($label), $totpSecret], - $globalOtpauthUrl - ); -} else { - $label = urlencode("FileRise:" . $username); - $issuer = urlencode("FileRise"); - $otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}"; -} - -// Build the QR code using Endroid QR Code. -$result = Builder::create() - ->writer(new PngWriter()) - ->data($otpauthUrl) - ->build(); - -header('Content-Type: ' . $result->getMimeType()); -echo $result->getString(); -?> \ No newline at end of file diff --git a/totp_verify.php b/totp_verify.php deleted file mode 100644 index 979f87d..0000000 --- a/totp_verify.php +++ /dev/null @@ -1,157 +0,0 @@ - $status, - 'code' => $code, - 'message' => $message, - 'data' => $data - ]); - exit; - } - - // Rate‑limit TOTP attempts - if (!isset($_SESSION['totp_failures'])) { - $_SESSION['totp_failures'] = 0; - } - if ($_SESSION['totp_failures'] >= 5) { - respond('error', 429, 'Too many TOTP attempts. Please try again later.'); - } - - /** - * Helper: Get a user's role from users.txt - */ - function getUserRole(string $username): ?string { - $usersFile = USERS_DIR . USERS_FILE; - if (!file_exists($usersFile)) return null; - foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { - $parts = explode(':', trim($line)); - if (count($parts) >= 3 && $parts[0] === $username) { - return trim($parts[2]); - } - } - return null; - } - - // Must be authenticated or pending TOTP - if ( - !( - (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) - || isset($_SESSION['pending_login_user']) - ) - ) { - respond('error', 403, 'Not authenticated'); - } - - $headers = array_change_key_case(getallheaders(), CASE_LOWER); - $csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; - - if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - respond('error', 403, 'Invalid CSRF token'); - } - - // Parse & validate input - $input = json_decode(file_get_contents("php://input"), true); - $code = trim($input['totp_code'] ?? ''); - if (!preg_match('/^\d{6}$/', $code)) { - respond('error', 400, 'A valid 6-digit TOTP code is required'); - } - - // LOGIN flow (Basic‑Auth or OIDC) - if (isset($_SESSION['pending_login_user'])) { - $username = $_SESSION['pending_login_user']; - $totpSecret = $_SESSION['pending_login_secret']; - $tfa = new \RobThree\Auth\TwoFactorAuth( - new GoogleChartsQrCodeProvider(), // QR code provider - 'FileRise', // issuer - 6, // number of digits - 30, // period in seconds - Algorithm::Sha1 // Correct enum case name from your enum - ); - - if (!$tfa->verifyCode($totpSecret, $code)) { - $_SESSION['totp_failures']++; - respond('error', 400, 'Invalid TOTP code'); - } - - // success → complete login - session_regenerate_id(true); - $_SESSION['authenticated'] = true; - $_SESSION['username'] = $username; - $_SESSION['isAdmin'] = (getUserRole($username) === "1"); - $_SESSION['folderOnly'] = loadUserPermissions($username); - - unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']); - - respond('ok', 200, 'Login successful'); - } - - // SETUP‑VERIFICATION flow - $username = $_SESSION['username'] ?? ''; - if (!$username) { - respond('error', 400, 'Username not found in session'); - } - - /** - * Helper: retrieve the user's TOTP secret from users.txt - */ - function getUserTOTPSecret(string $username): ?string { - global $encryptionKey; - $usersFile = USERS_DIR . USERS_FILE; - if (!file_exists($usersFile)) return null; - foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { - $parts = explode(':', trim($line)); - if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { - return decryptData($parts[3], $encryptionKey); - } - } - return null; - } - - $totpSecret = getUserTOTPSecret($username); - if (!$totpSecret) { - respond('error', 500, 'TOTP secret not found. Please set up TOTP again.'); - } - - $tfa = new \RobThree\Auth\TwoFactorAuth( - new GoogleChartsQrCodeProvider(), // QR code provider - 'FileRise', // issuer - 6, // number of digits - 30, // period in seconds - Algorithm::Sha1 // Correct enum case name from your enum - ); - - if (!$tfa->verifyCode($totpSecret, $code)) { - $_SESSION['totp_failures']++; - respond('error', 400, 'Invalid TOTP code'); - } - - // success - unset($_SESSION['totp_failures']); - respond('ok', 200, 'TOTP successfully verified'); - -} catch (\Throwable $e) { - // log error internally, then generic response - error_log("totp_verify error: " . $e->getMessage()); - http_response_code(500); - echo json_encode([ - 'status' => 'error', - 'code' => 500, - 'message' => 'Internal server error' - ]); - exit; -} \ No newline at end of file diff --git a/updateConfig.php b/updateConfig.php deleted file mode 100644 index de8d0bf..0000000 --- a/updateConfig.php +++ /dev/null @@ -1,99 +0,0 @@ - 'Unauthorized access.']); - exit; -} - -// Validate CSRF token. -$receivedToken = ''; -if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) { - $receivedToken = trim($_SERVER['HTTP_X_CSRF_TOKEN']); -} else { - $headers = array_change_key_case(getallheaders(), CASE_LOWER); - $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; -} -if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) { - http_response_code(403); - echo json_encode(['error' => 'Invalid CSRF token.']); - exit; -} - -// Retrieve and decode JSON input. -$input = file_get_contents('php://input'); -$data = json_decode($input, true); -if (!is_array($data)) { - http_response_code(400); - echo json_encode(['error' => 'Invalid input.']); - exit; -} - -// Retrieve new header title, sanitize if necessary. -$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : ""; - -// Validate and sanitize OIDC configuration. -$oidc = isset($data['oidc']) ? $data['oidc'] : []; -$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : ''; -$oidcClientId = isset($oidc['clientId']) ? trim($oidc['clientId']) : ''; -$oidcClientSecret = isset($oidc['clientSecret']) ? trim($oidc['clientSecret']) : ''; -$oidcRedirectUri = isset($oidc['redirectUri']) ? filter_var($oidc['redirectUri'], FILTER_SANITIZE_URL) : ''; - -if (!$oidcProviderUrl || !$oidcClientId || !$oidcClientSecret || !$oidcRedirectUri) { - http_response_code(400); - echo json_encode(['error' => 'Incomplete OIDC configuration.']); - exit; -} - -// Validate login option booleans. -$disableFormLogin = isset($data['disableFormLogin']) ? filter_var($data['disableFormLogin'], FILTER_VALIDATE_BOOLEAN) : false; -$disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false; -$disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN) : false; - -// Retrieve the global OTPAuth URL (new field). If not provided, default to an empty string. -$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : ""; - -// Prepare configuration array including the header title. -$configUpdate = [ - 'header_title' => $headerTitle, // New field for the header title - 'oidc' => [ - 'providerUrl' => $oidcProviderUrl, - 'clientId' => $oidcClientId, - 'clientSecret' => $oidcClientSecret, - 'redirectUri' => $oidcRedirectUri, - ], - 'loginOptions' => [ - 'disableFormLogin' => $disableFormLogin, - 'disableBasicAuth' => $disableBasicAuth, - 'disableOIDCLogin' => $disableOIDCLogin, - ], - 'globalOtpauthUrl' => $globalOtpauthUrl -]; - -// Define the configuration file path. -$configFile = USERS_DIR . 'adminConfig.json'; - -// Convert and encrypt configuration. -$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT); -$encryptedContent = encryptData($plainTextConfig, $encryptionKey); - -// Attempt to write the new configuration. -if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) { - error_log("updateConfig.php: Initial write failed, attempting to delete the old configuration file."); - if (file_exists($configFile)) { - unlink($configFile); - } - if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) { - error_log("updateConfig.php: Failed to write configuration even after deletion."); - http_response_code(500); - echo json_encode(['error' => 'Failed to update configuration even after cleanup.']); - exit; - } -} - -echo json_encode(['success' => 'Configuration updated successfully.']); -?> \ No newline at end of file diff --git a/updateUserPanel.php b/updateUserPanel.php deleted file mode 100644 index fc8f3ad..0000000 --- a/updateUserPanel.php +++ /dev/null @@ -1,80 +0,0 @@ - "Unauthorized"]); - exit; -} - -// Verify the CSRF token from headers. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; -if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) { - http_response_code(403); - echo json_encode(["error" => "Invalid CSRF token"]); - exit; -} - -$data = json_decode(file_get_contents("php://input"), true); -if (!is_array($data)) { - http_response_code(400); - echo json_encode(["error" => "Invalid input"]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -if (!$username) { - http_response_code(400); - echo json_encode(["error" => "No username in session"]); - exit; -} - -$totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false; -$usersFile = USERS_DIR . USERS_FILE; - -/** - * Clears the TOTP secret for a given user by removing or emptying the fourth field. - * - * @param string $username - */ -function disableUserTOTP($username) { - global $usersFile; - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - $newLines = []; - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - // If the line doesn't have at least three parts, leave it alone. - if (count($parts) < 3) { - $newLines[] = $line; - continue; - } - if ($parts[0] === $username) { - // If a fourth field exists, clear it; otherwise, append an empty field. - if (count($parts) >= 4) { - $parts[3] = ""; - } else { - $parts[] = ""; - } - $newLines[] = implode(':', $parts); - } else { - $newLines[] = $line; - } - } - file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX); -} - -// If TOTP is disabled, clear the user's TOTP secret. -if (!$totp_enabled) { - disableUserTOTP($username); - echo json_encode(["success" => "User panel updated: TOTP disabled"]); - exit; -} else { - // If TOTP is enabled, do not change the stored secret. - echo json_encode(["success" => "User panel updated: TOTP remains enabled"]); - exit; -} -?> \ No newline at end of file diff --git a/updateUserPermissions.php b/updateUserPermissions.php deleted file mode 100644 index c5ebbb9..0000000 --- a/updateUserPermissions.php +++ /dev/null @@ -1,94 +0,0 @@ - "Unauthorized"]); - exit; -} - -// Verify the CSRF token from headers. -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$csrfToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; -if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) { - http_response_code(403); - echo json_encode(["error" => "Invalid CSRF token"]); - exit; -} - -// Read the POST input. -$input = json_decode(file_get_contents("php://input"), true); -if (!isset($input['permissions']) || !is_array($input['permissions'])) { - echo json_encode(["error" => "Invalid input"]); - exit; -} - -$permissions = $input['permissions']; -$permissionsFile = USERS_DIR . "userPermissions.json"; - -// Load existing permissions if available and decrypt. -if (file_exists($permissionsFile)) { - $encryptedContent = file_get_contents($permissionsFile); - $json = decryptData($encryptedContent, $encryptionKey); - $existingPermissions = json_decode($json, true); - if (!is_array($existingPermissions)) { - $existingPermissions = []; - } -} else { - $existingPermissions = []; -} - -// Load user roles from the users file (similar to getUsers.php) -$usersFile = USERS_DIR . USERS_FILE; -$userRoles = []; -if (file_exists($usersFile)) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - if (count($parts) >= 3) { - // Validate username format: - if (preg_match(REGEX_USER, $parts[0])) { - // Use a lowercase key for consistency. - $userRoles[strtolower($parts[0])] = trim($parts[2]); - } - } - } -} - -// Loop through each permission update. -foreach ($permissions as $perm) { - // Ensure username is provided. - if (!isset($perm['username'])) continue; - $username = $perm['username']; - - // Look up the user's role from the users file. - $role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null; - - // Skip updating permissions for admin users. - if ($role === "1") { - continue; - } - - // Update permissions: default any missing value to false. - $existingPermissions[strtolower($username)] = [ - 'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false, - 'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false, - 'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false - ]; -} - -// Convert the permissions array to JSON. -$plainText = json_encode($existingPermissions, JSON_PRETTY_PRINT); -// Encrypt the JSON data. -$encryptedData = encryptData($plainText, $encryptionKey); -// Save encrypted permissions back to the JSON file. -$result = file_put_contents($permissionsFile, $encryptedData); -if ($result === false) { - echo json_encode(["error" => "Failed to save user permissions."]); - exit; -} - -echo json_encode(["success" => "User permissions updated successfully."]); -?> \ No newline at end of file diff --git a/upload.php b/upload.php deleted file mode 100644 index 1c970bf..0000000 --- a/upload.php +++ /dev/null @@ -1,273 +0,0 @@ - "Invalid CSRF token"]); - exit; -} - -// Ensure user is authenticated. -if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - http_response_code(401); - echo json_encode(["error" => "Unauthorized"]); - exit; -} - -$username = $_SESSION['username'] ?? ''; -if ($username) { - $userPermissions = loadUserPermissions($username); - if (!empty($userPermissions['disableUpload'])) { - http_response_code(403); - echo json_encode(["error" => "Upload disabled for this user."]); - exit; - } -} - -/* - * Handle test chunk requests. - */ -if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['resumableTest'])) { - $chunkNumber = intval($_GET['resumableChunkNumber']); - $resumableIdentifier = $_GET['resumableIdentifier'] ?? ''; - $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; - $baseUploadDir = UPLOAD_DIR; - if ($folder !== 'root') { - $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; - } - $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; - $chunkFile = $tempDir . $chunkNumber; - echo json_encode(["status" => file_exists($chunkFile) ? "found" : "not found"]); - http_response_code(file_exists($chunkFile) ? 200 : 404); - exit; -} - -// --------------------- -// Chunked upload handling (POST requests) -// --------------------- -if (isset($_POST['resumableChunkNumber'])) { - $chunkNumber = intval($_POST['resumableChunkNumber']); - $totalChunks = intval($_POST['resumableTotalChunks']); - $chunkSize = intval($_POST['resumableChunkSize']); - $totalSize = intval($_POST['resumableTotalSize']); - $resumableIdentifier = $_POST['resumableIdentifier'] ?? ''; - $resumableFilename = urldecode(basename($_POST['resumableFilename'])); - - if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { - http_response_code(400); - echo json_encode(["error" => "Invalid file name: $resumableFilename"]); - exit; - } - - $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; - if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder name"]); - exit; - } - - // Determine the base upload directory. - $baseUploadDir = UPLOAD_DIR; - if ($folder !== 'root') { - $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; - } - if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { - http_response_code(500); - echo json_encode(["error" => "Failed to create upload directory"]); - exit; - } - - // Use a temporary directory for the chunks. - $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; - if (!is_dir($tempDir) && !mkdir($tempDir, 0775, true)) { - http_response_code(500); - echo json_encode(["error" => "Failed to create temporary chunk directory"]); - exit; - } - - // Ensure there is no PHP upload error. - if (!isset($_FILES["file"]) || $_FILES["file"]["error"] !== UPLOAD_ERR_OK) { - http_response_code(400); - echo json_encode(["error" => "Upload error on chunk $chunkNumber"]); - exit; - } - - // Save the current chunk. - $chunkFile = $tempDir . $chunkNumber; - if (!move_uploaded_file($_FILES["file"]["tmp_name"], $chunkFile)) { - http_response_code(500); - echo json_encode(["error" => "Failed to move uploaded chunk $chunkNumber"]); - exit; - } - - // Check if all chunks have been uploaded by verifying each expected chunk. - $allChunksPresent = true; - for ($i = 1; $i <= $totalChunks; $i++) { - if (!file_exists($tempDir . $i)) { - $allChunksPresent = false; - break; - } - } - if (!$allChunksPresent) { - echo json_encode(["status" => "chunk uploaded"]); - exit; - } - - // All chunks are present. Merge the chunks. - $targetPath = $baseUploadDir . $resumableFilename; - if (!$out = fopen($targetPath, "wb")) { - http_response_code(500); - echo json_encode(["error" => "Failed to open target file for writing"]); - exit; - } - for ($i = 1; $i <= $totalChunks; $i++) { - $chunkPath = $tempDir . $i; - if (!file_exists($chunkPath)) { - fclose($out); - http_response_code(500); - echo json_encode(["error" => "Chunk $i missing during merge"]); - exit; - } - if (!$in = fopen($chunkPath, "rb")) { - fclose($out); - http_response_code(500); - echo json_encode(["error" => "Failed to open chunk $i"]); - exit; - } - while ($buff = fread($in, 4096)) { - fwrite($out, $buff); - } - fclose($in); - } - fclose($out); - - // --- Metadata Update for Chunked Upload --- - $folderPath = $folder; - $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath; - $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; - $metadataFile = META_DIR . $metadataFileName; - $uploadedDate = date(DATE_TIME_FORMAT); - $uploader = $_SESSION['username'] ?? "Unknown"; - - $metadataCollection = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; - if (!is_array($metadataCollection)) { - $metadataCollection = []; - } - if (!isset($metadataCollection[$resumableFilename])) { - $metadataCollection[$resumableFilename] = [ - "uploaded" => $uploadedDate, - "uploader" => $uploader - ]; - file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT)); - } - // --- End Metadata Update --- - - // Cleanup: use a robust recursive function. - function rrmdir($dir) { - if (!is_dir($dir)) return; - $items = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($items as $item) { - $item->isDir() ? rmdir($item->getRealPath()) : unlink($item->getRealPath()); - } - rmdir($dir); - } - rrmdir($tempDir); - - echo json_encode(["success" => "File uploaded successfully"]); - exit; -} else { - // ------------- Full Upload (Non-chunked) ------------- - $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; - if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { - http_response_code(400); - echo json_encode(["error" => "Invalid folder name"]); - exit; - } - - $baseUploadDir = UPLOAD_DIR; - if ($folder !== 'root') { - $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; - } - if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { - http_response_code(500); - echo json_encode(["error" => "Failed to create upload directory"]); - exit; - } - - $metadataCollection = []; - $metadataChanged = []; - $safeFileNamePattern = REGEX_FILE_NAME; - - foreach ($_FILES["file"]["name"] as $index => $fileName) { - $safeFileName = trim(urldecode(basename($fileName))); - if (!preg_match($safeFileNamePattern, $safeFileName)) { - http_response_code(400); - echo json_encode(["error" => "Invalid file name: " . $fileName]); - exit; - } - $relativePath = ''; - if (isset($_POST['relativePath'])) { - $relativePath = is_array($_POST['relativePath']) ? $_POST['relativePath'][$index] ?? '' : $_POST['relativePath']; - } - $folderPath = $folder; - $uploadDir = $baseUploadDir; - if (!empty($relativePath)) { - $subDir = dirname($relativePath); - if ($subDir !== '.' && $subDir !== '') { - $folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir; - $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR; - } - $safeFileName = basename($relativePath); - } - if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) { - http_response_code(500); - echo json_encode(["error" => "Failed to create subfolder"]); - exit; - } - $targetPath = $uploadDir . $safeFileName; - if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) { - $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath; - $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; - $metadataFile = META_DIR . $metadataFileName; - if (!isset($metadataCollection[$metadataKey])) { - $metadataCollection[$metadataKey] = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; - if (!is_array($metadataCollection[$metadataKey])) { - $metadataCollection[$metadataKey] = []; - } - $metadataChanged[$metadataKey] = false; - } - if (!isset($metadataCollection[$metadataKey][$safeFileName])) { - $uploadedDate = date(DATE_TIME_FORMAT); - $uploader = $_SESSION['username'] ?? "Unknown"; - $metadataCollection[$metadataKey][$safeFileName] = [ - "uploaded" => $uploadedDate, - "uploader" => $uploader - ]; - $metadataChanged[$metadataKey] = true; - } - } else { - http_response_code(500); - echo json_encode(["error" => "Error uploading file"]); - exit; - } - } - - foreach ($metadataCollection as $folderKey => $data) { - if ($metadataChanged[$folderKey]) { - $metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json'; - $metadataFile = META_DIR . $metadataFileName; - file_put_contents($metadataFile, json_encode($data, JSON_PRETTY_PRINT)); - } - } - - echo json_encode(["success" => "Files uploaded successfully"]); -} -?> \ No newline at end of file diff --git a/uploadToSharedFolder.php b/uploadToSharedFolder.php deleted file mode 100644 index d4fd49a..0000000 --- a/uploadToSharedFolder.php +++ /dev/null @@ -1,151 +0,0 @@ - "Method not allowed."]); - exit; -} - -// Ensure the share token is provided. -if (empty($_POST['token'])) { - http_response_code(400); - echo json_encode(["error" => "Missing share token."]); - exit; -} - -$token = trim($_POST['token']); - -// Load the share folder records. -$shareFile = META_DIR . "share_folder_links.json"; -if (!file_exists($shareFile)) { - http_response_code(404); - echo json_encode(["error" => "Share record not found."]); - exit; -} - -$shareLinks = json_decode(file_get_contents($shareFile), true); -if (!is_array($shareLinks) || !isset($shareLinks[$token])) { - http_response_code(404); - echo json_encode(["error" => "Invalid share token."]); - exit; -} - -$record = $shareLinks[$token]; - -// Check if the share link is expired. -if (time() > $record['expires']) { - http_response_code(403); - echo json_encode(["error" => "This share link has expired."]); - exit; -} - -// Ensure that uploads are allowed for this share. -if (empty($record['allowUpload']) || $record['allowUpload'] != 1) { - http_response_code(403); - echo json_encode(["error" => "File uploads are not allowed for this share."]); - exit; -} - -// Check that a file was uploaded. -if (!isset($_FILES['fileToUpload'])) { - http_response_code(400); - echo json_encode(["error" => "No file was uploaded."]); - exit; -} - -$fileUpload = $_FILES['fileToUpload']; - -// Check for upload errors. -if ($fileUpload['error'] !== UPLOAD_ERR_OK) { - http_response_code(400); - echo json_encode(["error" => "File upload error. Code: " . $fileUpload['error']]); - exit; -} - -// Enforce a maximum file size (e.g. 50MB). -$maxSize = 50 * 1024 * 1024; // 50MB -if ($fileUpload['size'] > $maxSize) { - http_response_code(400); - echo json_encode(["error" => "File size exceeds allowed limit."]); - exit; -} - -// Define allowed file extensions. -$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv']; -$uploadedName = basename($fileUpload['name']); -$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION)); -if (!in_array($ext, $allowedExtensions)) { - http_response_code(400); - echo json_encode(["error" => "File type not allowed."]); - exit; -} - -// Determine the target folder from the share record. -$folder = trim($record['folder'], "/\\"); -$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; -$realTargetFolder = realpath($targetFolder); -$uploadDirReal = realpath(UPLOAD_DIR); - -if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) { - http_response_code(404); - echo json_encode(["error" => "Shared folder not found."]); - exit; -} - -// Generate a new filename to avoid collisions. -// A unique prefix (using uniqid) is prepended to help with uniqueness and traceability. -$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName); -$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename; - -// Move the uploaded file securely. -if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) { - http_response_code(500); - echo json_encode(["error" => "Failed to move the uploaded file."]); - exit; -} - -// --- Metadata Update for Shared Upload --- -$metadataKey = ($folder === '' || $folder === 'root') ? "root" : $folder; -// Sanitize the metadata file name. -$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; -$metadataFile = META_DIR . $metadataFileName; - -// Load existing metadata if available. -$metadataCollection = []; -if (file_exists($metadataFile)) { - $data = file_get_contents($metadataFile); - $metadataCollection = json_decode($data, true); - if (!is_array($metadataCollection)) { - $metadataCollection = []; - } -} - -// Set upload date using your defined format. -$uploadedDate = date(DATE_TIME_FORMAT); - -// Since there is no logged-in user for public share uploads, -$uploader = "Outside Share"; - -// Update metadata for the new file. -if (!isset($metadataCollection[$newFilename])) { - $metadataCollection[$newFilename] = [ - "uploaded" => $uploadedDate, - "uploader" => $uploader - ]; -} - -// Save the metadata. -file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT)); -// --- End Metadata Update --- - -// Optionally, set a flash message in session. -$_SESSION['upload_message'] = "File uploaded successfully."; - -// Redirect back to the shared folder view, refreshing the file listing. -header("Location: shareFolder.php?token=" . urlencode($token)); -exit; -?> \ No newline at end of file