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