"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 = $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 Check --- if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { http_response_code(401); echo json_encode(["error" => "Unauthorized"]); exit; } $username = $_SESSION['username'] ?? ''; // --- Read‑only check --- $userPermissions = loadUserPermissions($username); if ($username && !empty($userPermissions['readOnly'])) { echo json_encode(["error" => "Read-only users are not allowed to save files."]); exit; } // --- Input parsing --- $data = json_decode(file_get_contents("php://input"), true); if (empty($data) || !isset($data["fileName"], $data["content"])) { http_response_code(400); echo json_encode(["error" => "Invalid request data", "received" => $data]); exit; } $fileName = basename($data["fileName"]); $folder = isset($data["folder"]) ? trim($data["folder"]) : "root"; // --- Folder validation --- if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) { echo json_encode(["error" => "Invalid folder name"]); exit; } $folder = trim($folder, "/\\ "); // --- Delegate to model, passing the uploader --- // Make sure FileModel::saveFile signature is: // saveFile(string $folder, string $fileName, $content, ?string $uploader = null) $result = FileModel::saveFile( $folder, $fileName, $data["content"], $username // ← pass the real uploader here ); 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"); ?>