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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create Folder
+
+
+
+
+
+
+
+
+
+
+
Rename Folder
+
+
+
+
+
+
+
+
+
+
+
+
+
Delete Folder
+
Are you sure you want to
+ delete this folder?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Files in (Root)
+
+
+
+
+
Delete Selected Files
+
Are you sure you want to delete the
+ selected files?
+
+
+
+
+
+
+
Copy Selected Files
+
Select a target folder for copying the
+ selected files:
+
+
+
+
+
+
+
+
Move Selected Files
+
Select a target folder for moving the
+ selected files:
+
+
+
+
+
+
+
+
+
Download Selected Files as Zip
+
Enter a name for the zip file:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
autorenew
+
Preparing your download...
+
+
+
+
+
+
+
Download File
+
Confirm or change the download file name:
+
+
+
+
+
+
+
+
+
+
+
+
+
Create New User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Remove User
+
+
+
+
+
+
+
+
+
+
+
Rename File
+
+
+
+
+
+
+
+
+
+
+
+
\ 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:
-
-
-
-
-
-
-
-
-
-
-
-
-
This folder is empty.
-
-
-
-
- | Filename |
- Size |
-
-
-
-
-
- |
-
-
- ⇩
-
- |
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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:
+
+
+
+
+
+
+
+
+
+
+
+
+
This folder is empty.
+
+
+
+
+ | Filename |
+ Size |
+
+
+
+
+
+ |
+
+
+ ⇩
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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