"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) { http_response_code(403); echo json_encode([ "success" => false, "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)) { http_response_code(400); echo json_encode(['error' => 'Invalid folder name.']); exit; } // Optionally sanitize the parent. if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) { http_response_code(400); 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'); if (empty($_SESSION['authenticated'])) { http_response_code(401); echo json_encode(["error" => "Unauthorized"]); exit; } $parent = $_GET['folder'] ?? null; $folderList = FolderModel::getFolderList($parent); 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. */ 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"; } } 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; } // Load admin config so we can pull the sharedMaxUploadSize require_once PROJECT_ROOT . '/src/models/AdminModel.php'; $adminConfig = AdminModel::getConfig(); $sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize']) ? (int)$adminConfig['sharedMaxUploadSize'] : null; // For human‐readable formatting 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"; } } // Extract data for the HTML view. $folderName = $data['folder']; $files = $data['files']; $currentPage = $data['currentPage']; $totalPages = $data['totalPages']; // Build the HTML view. header("Content-Type: text/html; charset=utf-8"); ?> Shared Folder: <?php echo htmlspecialchars($folderName, ENT_QUOTES, 'UTF-8'); ?>

Shared Folder:

This folder is empty.

Filename Size

Upload File ( max size)



"Unauthorized"]); exit; } // Read-only check $username = $_SESSION['username'] ?? ''; $perms = loadUserPermissions($username); if ($username && !empty($perms['readOnly'])) { http_response_code(403); echo json_encode(["error" => "Read-only users are not allowed to create share folders."]); exit; } // Input $in = json_decode(file_get_contents("php://input"), true); if (!$in || !isset($in['folder'])) { http_response_code(400); echo json_encode(["error" => "Invalid input."]); exit; } $folder = trim($in['folder']); $value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60; $unit = $in['expirationUnit'] ?? 'minutes'; $password = $in['password'] ?? ''; $allowUpload = intval($in['allowUpload'] ?? 0); // Folder name validation if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; } // Convert to seconds switch ($unit) { case 'seconds': $seconds = $value; break; case 'hours': $seconds = $value * 3600; break; case 'days': $seconds = $value * 86400; break; case 'minutes': default: $seconds = $value * 60; break; } // Delegate $res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload); echo json_encode($res); 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; } /** * GET /api/folder/getShareFolderLinks.php */ public function getAllShareFolderLinks(): void { header('Content-Type: application/json'); $shareFile = META_DIR . 'share_folder_links.json'; $links = file_exists($shareFile) ? json_decode(file_get_contents($shareFile), true) ?? [] : []; $now = time(); $cleaned = []; // 1) Remove expired foreach ($links as $token => $record) { if (!empty($record['expires']) && $record['expires'] < $now) { continue; } $cleaned[$token] = $record; } // 2) Persist back if anything was pruned if (count($cleaned) !== count($links)) { file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT)); } echo json_encode($cleaned); } /** * POST /api/folder/deleteShareFolderLink.php */ public function deleteShareFolderLink() { header('Content-Type: application/json'); $token = $_POST['token'] ?? ''; if (!$token) { echo json_encode(['success' => false, 'error' => 'No token provided']); return; } $deleted = FolderModel::deleteShareFolderLink($token); if ($deleted) { echo json_encode(['success' => true]); } else { echo json_encode(['success' => false, 'error' => 'Not found']); } } }