diff --git a/public/api/addUser.php b/public/api/addUser.php index 7bed442..9c90059 100644 --- a/public/api/addUser.php +++ b/public/api/addUser.php @@ -2,7 +2,7 @@ // public/api/addUser.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->addUser(); \ No newline at end of file diff --git a/public/api/admin/getConfig.php b/public/api/admin/getConfig.php index fba6d4a..baf2182 100644 --- a/public/api/admin/getConfig.php +++ b/public/api/admin/getConfig.php @@ -2,7 +2,7 @@ // public/api/admin/getConfig.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/adminController.php'; +require_once PROJECT_ROOT . '/src/controllers/AdminController.php'; $adminController = new AdminController(); $adminController->getConfig(); \ No newline at end of file diff --git a/public/api/admin/readMetadata.php b/public/api/admin/readMetadata.php new file mode 100644 index 0000000..5a5baae --- /dev/null +++ b/public/api/admin/readMetadata.php @@ -0,0 +1,44 @@ +'Forbidden']); + exit; +} + +// Expect a ?file=share_links.json or share_folder_links.json +if (empty($_GET['file'])) { + http_response_code(400); + echo json_encode(['error'=>'Missing `file` parameter']); + exit; +} + +$file = basename($_GET['file']); +$allowed = ['share_links.json','share_folder_links.json']; +if (!in_array($file, $allowed, true)) { + http_response_code(403); + echo json_encode(['error'=>'Invalid file requested']); + exit; +} + +$path = META_DIR . $file; +if (!file_exists($path)) { + http_response_code(404); + echo json_encode((object)[]); // return empty object + exit; +} + +$data = file_get_contents($path); +$json = json_decode($data, true); +if (json_last_error() !== JSON_ERROR_NONE) { + http_response_code(500); + echo json_encode(['error'=>'Corrupted JSON']); + exit; +} + +header('Content-Type: application/json'); +echo json_encode($json); \ No newline at end of file diff --git a/public/api/admin/updateConfig.php b/public/api/admin/updateConfig.php index 9aeb185..412031c 100644 --- a/public/api/admin/updateConfig.php +++ b/public/api/admin/updateConfig.php @@ -2,7 +2,7 @@ // public/api/admin/updateConfig.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/adminController.php'; +require_once PROJECT_ROOT . '/src/controllers/AdminController.php'; $adminController = new AdminController(); $adminController->updateConfig(); \ No newline at end of file diff --git a/public/api/auth/auth.php b/public/api/auth/auth.php index a31d8f9..694b8cf 100644 --- a/public/api/auth/auth.php +++ b/public/api/auth/auth.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/../../../config/config.php'; require_once PROJECT_ROOT . '/vendor/autoload.php'; -require_once PROJECT_ROOT . '/src/controllers/authController.php'; +require_once PROJECT_ROOT . '/src/controllers/AuthController.php'; $authController = new AuthController(); $authController->auth(); \ No newline at end of file diff --git a/public/api/auth/checkAuth.php b/public/api/auth/checkAuth.php index f274438..379d824 100644 --- a/public/api/auth/checkAuth.php +++ b/public/api/auth/checkAuth.php @@ -2,7 +2,7 @@ // public/api/auth/checkAuth.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/authController.php'; +require_once PROJECT_ROOT . '/src/controllers/AuthController.php'; $authController = new AuthController(); $authController->checkAuth(); \ No newline at end of file diff --git a/public/api/auth/login_basic.php b/public/api/auth/login_basic.php index cd81ef8..23e9559 100644 --- a/public/api/auth/login_basic.php +++ b/public/api/auth/login_basic.php @@ -2,7 +2,7 @@ // public/api/auth/login_basic.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/authController.php'; +require_once PROJECT_ROOT . '/src/controllers/AuthController.php'; $authController = new AuthController(); $authController->loginBasic(); \ No newline at end of file diff --git a/public/api/auth/logout.php b/public/api/auth/logout.php index 3fe9012..d571c89 100644 --- a/public/api/auth/logout.php +++ b/public/api/auth/logout.php @@ -2,7 +2,7 @@ // public/api/auth/logout.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/authController.php'; +require_once PROJECT_ROOT . '/src/controllers/AuthController.php'; $authController = new AuthController(); $authController->logout(); \ No newline at end of file diff --git a/public/api/auth/token.php b/public/api/auth/token.php index e6dbc59..7a0fe1d 100644 --- a/public/api/auth/token.php +++ b/public/api/auth/token.php @@ -2,7 +2,7 @@ // public/api/auth/token.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/authController.php'; +require_once PROJECT_ROOT . '/src/controllers/AuthController.php'; $authController = new AuthController(); $authController->getToken(); \ No newline at end of file diff --git a/public/api/changePassword.php b/public/api/changePassword.php index 468287f..5535ed9 100644 --- a/public/api/changePassword.php +++ b/public/api/changePassword.php @@ -2,7 +2,7 @@ // public/api/changePassword.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->changePassword(); \ No newline at end of file diff --git a/public/api/file/copyFiles.php b/public/api/file/copyFiles.php index 2afed0d..bb83d7a 100644 --- a/public/api/file/copyFiles.php +++ b/public/api/file/copyFiles.php @@ -2,7 +2,7 @@ // public/api/file/copyFiles.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->copyFiles(); \ No newline at end of file diff --git a/public/api/file/createShareLink.php b/public/api/file/createShareLink.php index 6d5ac35..cddae48 100644 --- a/public/api/file/createShareLink.php +++ b/public/api/file/createShareLink.php @@ -2,7 +2,7 @@ // public/api/file/createShareLink.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->createShareLink(); \ No newline at end of file diff --git a/public/api/file/deleteFiles.php b/public/api/file/deleteFiles.php index 83c95c9..a6116f6 100644 --- a/public/api/file/deleteFiles.php +++ b/public/api/file/deleteFiles.php @@ -2,7 +2,7 @@ // public/api/file/deleteFiles.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->deleteFiles(); \ No newline at end of file diff --git a/public/api/file/deleteShareLink.php b/public/api/file/deleteShareLink.php new file mode 100644 index 0000000..59d8eef --- /dev/null +++ b/public/api/file/deleteShareLink.php @@ -0,0 +1,6 @@ +deleteShareLink(); \ No newline at end of file diff --git a/public/api/file/deleteTrashFiles.php b/public/api/file/deleteTrashFiles.php index 5f11c7a..a9d5153 100644 --- a/public/api/file/deleteTrashFiles.php +++ b/public/api/file/deleteTrashFiles.php @@ -2,7 +2,7 @@ // public/api/file/deleteTrashFiles.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->deleteTrashFiles(); \ No newline at end of file diff --git a/public/api/file/download.php b/public/api/file/download.php index d74c427..3bd6b87 100644 --- a/public/api/file/download.php +++ b/public/api/file/download.php @@ -2,7 +2,7 @@ // public/api/file/download.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->downloadFile(); \ No newline at end of file diff --git a/public/api/file/downloadZip.php b/public/api/file/downloadZip.php index 7d17c85..08b7c7c 100644 --- a/public/api/file/downloadZip.php +++ b/public/api/file/downloadZip.php @@ -2,7 +2,7 @@ // public/api/file/downloadZip.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->downloadZip(); \ No newline at end of file diff --git a/public/api/file/extractZip.php b/public/api/file/extractZip.php index f66efb3..8208900 100644 --- a/public/api/file/extractZip.php +++ b/public/api/file/extractZip.php @@ -2,7 +2,7 @@ // public/api/file/extractZip.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->extractZip(); \ No newline at end of file diff --git a/public/api/file/getFileList.php b/public/api/file/getFileList.php index 5345663..d2c94d2 100644 --- a/public/api/file/getFileList.php +++ b/public/api/file/getFileList.php @@ -2,7 +2,7 @@ // public/api/file/getFileList.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->getFileList(); \ No newline at end of file diff --git a/public/api/file/getFileTag.php b/public/api/file/getFileTag.php index 76c0e21..d09bc54 100644 --- a/public/api/file/getFileTag.php +++ b/public/api/file/getFileTag.php @@ -2,7 +2,7 @@ // public/api/file/getFileTag.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->getFileTags(); \ No newline at end of file diff --git a/public/api/file/getShareLinks.php b/public/api/file/getShareLinks.php new file mode 100644 index 0000000..8e72534 --- /dev/null +++ b/public/api/file/getShareLinks.php @@ -0,0 +1,6 @@ +getShareLinks(); \ No newline at end of file diff --git a/public/api/file/getTrashItems.php b/public/api/file/getTrashItems.php index 9bcf475..1d72f72 100644 --- a/public/api/file/getTrashItems.php +++ b/public/api/file/getTrashItems.php @@ -2,7 +2,7 @@ // public/api/file/getTrashItems.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->getTrashItems(); \ No newline at end of file diff --git a/public/api/file/moveFiles.php b/public/api/file/moveFiles.php index 040890c..659340d 100644 --- a/public/api/file/moveFiles.php +++ b/public/api/file/moveFiles.php @@ -2,7 +2,7 @@ // public/api/file/moveFiles.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->moveFiles(); \ No newline at end of file diff --git a/public/api/file/renameFile.php b/public/api/file/renameFile.php index a3f5da8..1529926 100644 --- a/public/api/file/renameFile.php +++ b/public/api/file/renameFile.php @@ -2,7 +2,7 @@ // public/api/file/renameFile.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->renameFile(); \ No newline at end of file diff --git a/public/api/file/restoreFiles.php b/public/api/file/restoreFiles.php index 3584741..453ad57 100644 --- a/public/api/file/restoreFiles.php +++ b/public/api/file/restoreFiles.php @@ -2,7 +2,7 @@ // public/api/file/restoreFiles.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->restoreFiles(); \ No newline at end of file diff --git a/public/api/file/saveFile.php b/public/api/file/saveFile.php index 29d1cb5..6ea3eb1 100644 --- a/public/api/file/saveFile.php +++ b/public/api/file/saveFile.php @@ -2,7 +2,7 @@ // public/api/file/saveFile.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->saveFile(); \ No newline at end of file diff --git a/public/api/file/saveFileTag.php b/public/api/file/saveFileTag.php index 87f5ae4..528ae91 100644 --- a/public/api/file/saveFileTag.php +++ b/public/api/file/saveFileTag.php @@ -2,7 +2,7 @@ // public/api/file/saveFileTag.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->saveFileTag(); \ No newline at end of file diff --git a/public/api/file/share.php b/public/api/file/share.php index 44971cd..df26dbc 100644 --- a/public/api/file/share.php +++ b/public/api/file/share.php @@ -2,7 +2,7 @@ // public/api/file/share.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/fileController.php'; +require_once PROJECT_ROOT . '/src/controllers/FileController.php'; $fileController = new FileController(); $fileController->shareFile(); \ No newline at end of file diff --git a/public/api/folder/createFolder.php b/public/api/folder/createFolder.php index 77defd5..0143bb7 100644 --- a/public/api/folder/createFolder.php +++ b/public/api/folder/createFolder.php @@ -2,7 +2,7 @@ // public/api/folder/createFolder.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->createFolder(); \ No newline at end of file diff --git a/public/api/folder/createShareFolderLink.php b/public/api/folder/createShareFolderLink.php index 126b961..e6882da 100644 --- a/public/api/folder/createShareFolderLink.php +++ b/public/api/folder/createShareFolderLink.php @@ -2,7 +2,7 @@ // public/api/folder/createShareFolderLink.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->createShareFolderLink(); \ No newline at end of file diff --git a/public/api/folder/deleteFolder.php b/public/api/folder/deleteFolder.php index 8bcacbc..f9039d2 100644 --- a/public/api/folder/deleteFolder.php +++ b/public/api/folder/deleteFolder.php @@ -2,7 +2,7 @@ // public/api/folder/deleteFolder.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->deleteFolder(); \ No newline at end of file diff --git a/public/api/folder/deleteShareFolderLink.php b/public/api/folder/deleteShareFolderLink.php new file mode 100644 index 0000000..91871a9 --- /dev/null +++ b/public/api/folder/deleteShareFolderLink.php @@ -0,0 +1,6 @@ +deleteShareFolderLink(); \ No newline at end of file diff --git a/public/api/folder/downloadSharedFile.php b/public/api/folder/downloadSharedFile.php index 07c69d8..1dea168 100644 --- a/public/api/folder/downloadSharedFile.php +++ b/public/api/folder/downloadSharedFile.php @@ -2,7 +2,7 @@ // public/api/folder/downloadSharedFile.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->downloadSharedFile(); \ No newline at end of file diff --git a/public/api/folder/getFolderList.php b/public/api/folder/getFolderList.php index 9c44509..7693354 100644 --- a/public/api/folder/getFolderList.php +++ b/public/api/folder/getFolderList.php @@ -2,7 +2,7 @@ // public/api/folder/getFolderList.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->getFolderList(); \ No newline at end of file diff --git a/public/api/folder/getShareFolderLinks.php b/public/api/folder/getShareFolderLinks.php new file mode 100644 index 0000000..204c4b7 --- /dev/null +++ b/public/api/folder/getShareFolderLinks.php @@ -0,0 +1,6 @@ +getShareFolderLinks(); \ No newline at end of file diff --git a/public/api/folder/renameFolder.php b/public/api/folder/renameFolder.php index ce9a344..8007ac9 100644 --- a/public/api/folder/renameFolder.php +++ b/public/api/folder/renameFolder.php @@ -2,7 +2,7 @@ // public/api/folder/renameFolder.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->renameFolder(); \ No newline at end of file diff --git a/public/api/folder/shareFolder.php b/public/api/folder/shareFolder.php index 6773a38..9bb11de 100644 --- a/public/api/folder/shareFolder.php +++ b/public/api/folder/shareFolder.php @@ -2,7 +2,7 @@ // public/api/folder/shareFolder.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->shareFolder(); \ No newline at end of file diff --git a/public/api/folder/uploadToSharedFolder.php b/public/api/folder/uploadToSharedFolder.php index 643110f..71fe584 100644 --- a/public/api/folder/uploadToSharedFolder.php +++ b/public/api/folder/uploadToSharedFolder.php @@ -2,7 +2,7 @@ // public/api/folder/uploadToSharedFolder.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/folderController.php'; +require_once PROJECT_ROOT . '/src/controllers/FolderController.php'; $folderController = new FolderController(); $folderController->uploadToSharedFolder(); \ No newline at end of file diff --git a/public/api/getUserPermissions.php b/public/api/getUserPermissions.php index 9ebd320..3e66bad 100644 --- a/public/api/getUserPermissions.php +++ b/public/api/getUserPermissions.php @@ -2,7 +2,7 @@ // public/api/getUserPermissions.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->getUserPermissions(); \ No newline at end of file diff --git a/public/api/getUsers.php b/public/api/getUsers.php index b68da7b..93f63eb 100644 --- a/public/api/getUsers.php +++ b/public/api/getUsers.php @@ -2,7 +2,7 @@ // public/api/getUsers.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->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 index 350e9de..636c2bd 100644 --- a/public/api/removeUser.php +++ b/public/api/removeUser.php @@ -2,7 +2,7 @@ // public/api/removeUser.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->removeUser(); \ No newline at end of file diff --git a/public/api/totp_disable.php b/public/api/totp_disable.php index a196559..0afb368 100644 --- a/public/api/totp_disable.php +++ b/public/api/totp_disable.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/../../config/config.php'; require_once PROJECT_ROOT . '/vendor/autoload.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->disableTOTP(); \ No newline at end of file diff --git a/public/api/totp_recover.php b/public/api/totp_recover.php index 32f36b4..0c3df39 100644 --- a/public/api/totp_recover.php +++ b/public/api/totp_recover.php @@ -2,7 +2,7 @@ // public/api/totp_recover.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->recoverTOTP(); \ No newline at end of file diff --git a/public/api/totp_saveCode.php b/public/api/totp_saveCode.php index 1b2cc95..d73c034 100644 --- a/public/api/totp_saveCode.php +++ b/public/api/totp_saveCode.php @@ -2,7 +2,7 @@ // public/api/totp_saveCode.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->saveTOTPRecoveryCode(); \ No newline at end of file diff --git a/public/api/totp_setup.php b/public/api/totp_setup.php index f9b8877..6d8bc7a 100644 --- a/public/api/totp_setup.php +++ b/public/api/totp_setup.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/../../config/config.php'; require_once PROJECT_ROOT . '/vendor/autoload.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->setupTOTP(); \ No newline at end of file diff --git a/public/api/totp_verify.php b/public/api/totp_verify.php index fdd48b2..0dd90f6 100644 --- a/public/api/totp_verify.php +++ b/public/api/totp_verify.php @@ -3,7 +3,7 @@ require_once __DIR__ . '/../../config/config.php'; require_once PROJECT_ROOT . '/vendor/autoload.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->verifyTOTP(); \ No newline at end of file diff --git a/public/api/updateUserPanel.php b/public/api/updateUserPanel.php index 54be7c4..e30f9e5 100644 --- a/public/api/updateUserPanel.php +++ b/public/api/updateUserPanel.php @@ -2,7 +2,7 @@ // public/api/updateUserPanel.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->updateUserPanel(); \ No newline at end of file diff --git a/public/api/updateUserPermissions.php b/public/api/updateUserPermissions.php index d5406f4..5e93832 100644 --- a/public/api/updateUserPermissions.php +++ b/public/api/updateUserPermissions.php @@ -2,7 +2,7 @@ // public/api/updateUserPermissions.php require_once __DIR__ . '/../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/userController.php'; +require_once PROJECT_ROOT . '/src/controllers/UserController.php'; $userController = new UserController(); $userController->updateUserPermissions(); \ No newline at end of file diff --git a/public/api/upload/removeChunks.php b/public/api/upload/removeChunks.php index 3fd3edf..2577c1c 100644 --- a/public/api/upload/removeChunks.php +++ b/public/api/upload/removeChunks.php @@ -2,7 +2,7 @@ // public/api/upload/removeChunks.php require_once __DIR__ . '/../../../config/config.php'; -require_once PROJECT_ROOT . '/src/controllers/uploadController.php'; +require_once PROJECT_ROOT . '/src/controllers/UploadController.php'; $uploadController = new UploadController(); $uploadController->removeChunks(); \ No newline at end of file diff --git a/public/api/upload/upload.php b/public/api/upload/upload.php index 195b2cb..3cf7e73 100644 --- a/public/api/upload/upload.php +++ b/public/api/upload/upload.php @@ -1,7 +1,7 @@ handleUpload(); \ No newline at end of file diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js new file mode 100644 index 0000000..41e2d5e --- /dev/null +++ b/public/js/adminPanel.js @@ -0,0 +1,652 @@ +import { t } from './i18n.js'; +import { loadAdminConfigFunc } from './auth.js'; +import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; +import { sendRequest } from './networkUtils.js'; + +const version = "v1.3.0"; +const adminTitle = `${t("admin_panel")} ${version}`; + +// ————— Inject updated styles ————— +(function(){ + if (document.getElementById('adminPanelStyles')) return; + const style = document.createElement('style'); + style.id = 'adminPanelStyles'; + style.textContent = ` + /* Modal sizing */ + #adminPanelModal .modal-content { + max-width: 1100px; + width: 50%; + } + + /* Small phones: 90% width */ + @media (max-width: 900px) { + #adminPanelModal .modal-content { + width: 90% !important; + max-width: none !important; + } + } + + /* Dark-mode fixes */ + body.dark-mode #adminPanelModal .modal-content { + border-color: #555 !important; + } + + /* enforce light‐mode styling */ + #adminPanelModal .modal-content { + max-width: 1100px; + width: 50%; + background: #fff !important; + color: #000 !important; + border: 1px solid #ccc !important; + } + + /* enforce dark‐mode styling */ + body.dark-mode #adminPanelModal .modal-content { + background: #2c2c2c !important; + color: #e0e0e0 !important; + border-color: #555 !important; + } + + /* form controls in dark */ + body.dark-mode .form-control { + background-color: #333; + border-color: #555; + color: #eee; + } + body.dark-mode .form-control::placeholder { color: #888; } + + /* Section headers */ + .section-header { + background: #f5f5f5; + padding: 10px 15px; + cursor: pointer; + border-radius: 4px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; + } + .section-header:first-of-type { margin-top: 0; } + .section-header.collapsed .material-icons { transform: rotate(-90deg); } + .section-header .material-icons { transition: transform .3s; color: #444; } + + body.dark-mode .section-header { + background: #3a3a3a; + color: #eee; + } + body.dark-mode .section-header .material-icons { color: #ccc; } + + /* Hidden by default */ + .section-content { + display: none; + margin-left: 20px; + margin-top: 8px; + margin-bottom: 8px; + } + + /* Close button */ + #adminPanelModal .editor-close-btn { + position: absolute; top:10px; right:10px; + display:flex; align-items:center; justify-content:center; + font-size:20px; font-weight:bold; cursor:pointer; + z-index:1000; width:32px; height:32px; border-radius:50%; + text-align:center; line-height:30px; + color:#ff4d4d; background:rgba(255,255,255,0.9); + border:2px solid transparent; transition:all .3s; + } + #adminPanelModal .editor-close-btn:hover { + color:white; background:#ff4d4d; + box-shadow:0 0 6px rgba(255,77,77,.8); + transform:scale(1.05); + } + body.dark-mode #adminPanelModal .editor-close-btn { + background:rgba(0,0,0,0.6); + color:#ff4d4d; + } + + /* Action-row */ + .action-row { + display:flex; + justify-content:space-between; + margin-top:15px; + } + `; + document.head.appendChild(style); + })(); +// ———————————————————————————————————— + +let originalAdminConfig = {}; +function captureInitialAdminConfig() { + originalAdminConfig = { + headerTitle: document.getElementById("headerTitle").value.trim(), + oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(), + oidcClientId: document.getElementById("oidcClientId").value.trim(), + oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(), + oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(), + disableFormLogin: document.getElementById("disableFormLogin").checked, + disableBasicAuth: document.getElementById("disableBasicAuth").checked, + disableOIDCLogin: document.getElementById("disableOIDCLogin").checked, + enableWebDAV: document.getElementById("enableWebDAV").checked, + sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(), + globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim() + }; +} +function hasUnsavedChanges() { + const o = originalAdminConfig; + return ( + document.getElementById("headerTitle").value.trim() !== o.headerTitle || + document.getElementById("oidcProviderUrl").value.trim() !== o.oidcProviderUrl || + document.getElementById("oidcClientId").value.trim() !== o.oidcClientId || + document.getElementById("oidcClientSecret").value.trim() !== o.oidcClientSecret || + document.getElementById("oidcRedirectUri").value.trim() !== o.oidcRedirectUri || + document.getElementById("disableFormLogin").checked !== o.disableFormLogin || + document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth || + document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin || + document.getElementById("enableWebDAV").checked !== o.enableWebDAV || + document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize || + document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl + ); +} + +function showCustomConfirmModal(message) { + return new Promise(resolve => { + const modal = document.getElementById("customConfirmModal"); + const msg = document.getElementById("confirmMessage"); + const yes = document.getElementById("confirmYesBtn"); + const no = document.getElementById("confirmNoBtn"); + msg.textContent = message; + modal.style.display = "block"; + function clean() { + modal.style.display = "none"; + yes.removeEventListener("click", onYes); + no.removeEventListener("click", onNo); + } + function onYes() { clean(); resolve(true); } + function onNo() { clean(); resolve(false); } + yes.addEventListener("click", onYes); + no.addEventListener("click", onNo); + }); +} + +function toggleSection(id) { + const hdr = document.getElementById(id + "Header"); + const cnt = document.getElementById(id + "Content"); + const isCollapsedNow = hdr.classList.toggle("collapsed"); + // collapsed class present => hide; absent => show + cnt.style.display = isCollapsedNow ? "none" : "block"; + if (!isCollapsedNow && id === "shareLinks") { + loadShareLinksSection(); + } +} + +function loadShareLinksSection() { + const container = document.getElementById("shareLinksContent"); + container.textContent = t("loading") + "..."; + + Promise.all([ + fetch("/api/admin/readMetadata.php?file=share_folder_links.json", { credentials: "include" }) + .then(r => r.ok ? r.json() : Promise.reject(`Folder fetch ${r.status}`)), + fetch("/api/admin/readMetadata.php?file=share_links.json", { credentials: "include" }) + .then(r => r.ok ? r.json() : Promise.reject(`File fetch ${r.status}`)) + ]) + .then(([folders, files]) => { + let html = `
" + t("no_users_found") + "
"; + return; + } + users.forEach(user => { + // Skip admin users. + if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return; + + // Use stored permissions if available; otherwise fall back to defaults. + const defaultPerm = { + folderOnly: false, + readOnly: false, + disableUpload: false, + }; + + // Normalize the username key to match server storage (e.g., lowercase) + const usernameKey = user.username.toLowerCase(); + + const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData)) + ? permissionsData[usernameKey] + : defaultPerm; + + // Create a row for the user. + const row = document.createElement("div"); + row.classList.add("user-permission-row"); + row.setAttribute("data-username", user.username); + row.style.padding = "10px 0"; + row.innerHTML = ` +" + t("error_loading_users") + "
"; + }); + } \ No newline at end of file diff --git a/public/js/auth.js b/public/js/auth.js index 07c0eb4..96678c4 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -15,10 +15,9 @@ import { openUserPanel, openTOTPModal, closeTOTPModal, - openAdminPanel, - closeAdminPanel, setLastLoginData } from './authModals.js'; +import { openAdminPanel } from './adminPanel.js'; // Production OIDC configuration (override via API as needed) const currentOIDCConfig = { diff --git a/public/js/authModals.js b/public/js/authModals.js index 5244443..4df26cc 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -3,8 +3,6 @@ import { sendRequest } from './networkUtils.js'; import { t, applyTranslations, setLocale } from './i18n.js'; import { loadAdminConfigFunc } from './auth.js'; -const version = "v1.2.8"; // Update this version string as needed -const adminTitle = `${t("admin_panel")} ${version}`; let lastLoginData = null; export function setLastLoginData(data) { @@ -544,539 +542,4 @@ export function closeTOTPModal(disable = true) { }) .catch(() => { showToast(t("error_disabling_totp_setting")); }); } -} - -// Global variable to hold the initial state of the admin form. -let originalAdminConfig = {}; - -// Capture the initial state of the admin form fields. -function captureInitialAdminConfig() { - originalAdminConfig = { - headerTitle: document.getElementById("headerTitle").value.trim(), - oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(), - oidcClientId: document.getElementById("oidcClientId").value.trim(), - oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(), - oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(), - disableFormLogin: document.getElementById("disableFormLogin").checked, - disableBasicAuth: document.getElementById("disableBasicAuth").checked, - disableOIDCLogin: document.getElementById("disableOIDCLogin").checked, - globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim() - }; -} - -// Compare current values to the captured initial state. -function hasUnsavedChanges() { - return ( - document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle || - document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl || - document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId || - document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret || - document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri || - document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin || - document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth || - document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin || - document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl - ); -} - -// Use your custom confirmation modal. -function showCustomConfirmModal(message) { - return new Promise((resolve) => { - // Get modal elements from DOM. - const modal = document.getElementById("customConfirmModal"); - const messageElem = document.getElementById("confirmMessage"); - const yesBtn = document.getElementById("confirmYesBtn"); - const noBtn = document.getElementById("confirmNoBtn"); - - // Set the message in the modal. - messageElem.textContent = message; - modal.style.display = "block"; - - // Define event handlers. - function onYes() { - cleanup(); - resolve(true); - } - function onNo() { - cleanup(); - resolve(false); - } - // Remove event listeners and hide modal after choice. - function cleanup() { - yesBtn.removeEventListener("click", onYes); - noBtn.removeEventListener("click", onNo); - modal.style.display = "none"; - } - - yesBtn.addEventListener("click", onYes); - noBtn.addEventListener("click", onNo); - }); -} - -export function openAdminPanel() { - fetch("/api/admin/getConfig.php", { credentials: "include" }) - .then(response => response.json()) - .then(config => { - if (config.header_title) { - document.querySelector(".header-title h1").textContent = config.header_title; - window.headerTitle = config.header_title || "FileRise"; - } - if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc); - if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; - - const isDarkMode = document.body.classList.contains("dark-mode"); - const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; - const modalContentStyles = ` - background: ${isDarkMode ? "#2c2c2c" : "#fff"}; - color: ${isDarkMode ? "#e0e0e0" : "#000"}; - padding: 20px; - max-width: 600px; - width: 90%; - border-radius: 8px; - position: relative; - overflow-y: auto; - max-height: 90vh; - border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"}; - `; - - let adminModal = document.getElementById("adminPanelModal"); - - if (!adminModal) { - adminModal = document.createElement("div"); - adminModal.id = "adminPanelModal"; - adminModal.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: ${overlayBackground}; - display: flex; - justify-content: center; - align-items: center; - z-index: 3000; - `; - adminModal.innerHTML = ` - - `; - document.body.appendChild(adminModal); - - // Bind closing - document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel); - adminModal.addEventListener("click", e => { if (e.target === adminModal) closeAdminPanel(); }); - document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel); - - // Bind other buttons - document.getElementById("adminOpenAddUser").addEventListener("click", () => { - toggleVisibility("addUserModal", true); - document.getElementById("newUsername").focus(); - }); - document.getElementById("adminOpenRemoveUser").addEventListener("click", () => { - if (typeof window.loadUserList === "function") window.loadUserList(); - toggleVisibility("removeUserModal", true); - }); - document.getElementById("adminOpenUserPermissions").addEventListener("click", () => { - openUserPermissionsModal(); - }); - - // Save handler - document.getElementById("saveAdminSettings").addEventListener("click", () => { - const disableFormLoginCheckbox = document.getElementById("disableFormLogin"); - const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth"); - const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin"); - const enableWebDAVCheckbox = document.getElementById("enableWebDAV"); - const sharedMaxUploadSizeInput = document.getElementById("sharedMaxUploadSize"); - - const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox] - .filter(cb => cb.checked).length; - if (totalDisabled === 3) { - showToast(t("at_least_one_login_method")); - disableOIDCLoginCheckbox.checked = false; - localStorage.setItem("disableOIDCLogin", "false"); - if (typeof window.updateLoginOptionsUI === "function") { - window.updateLoginOptionsUI({ - disableFormLogin: disableFormLoginCheckbox.checked, - disableBasicAuth: disableBasicAuthCheckbox.checked, - disableOIDCLogin: disableOIDCLoginCheckbox.checked - }); - } - return; - } - - const newHeaderTitle = document.getElementById("headerTitle").value.trim(); - const newOIDCConfig = { - providerUrl: document.getElementById("oidcProviderUrl").value.trim(), - clientId: document.getElementById("oidcClientId").value.trim(), - clientSecret: document.getElementById("oidcClientSecret").value.trim(), - redirectUri: document.getElementById("oidcRedirectUri").value.trim() - }; - const disableFormLogin = disableFormLoginCheckbox.checked; - const disableBasicAuth = disableBasicAuthCheckbox.checked; - const disableOIDCLogin = disableOIDCLoginCheckbox.checked; - const enableWebDAV = enableWebDAVCheckbox.checked; - const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0; - const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim(); - - sendRequest("/api/admin/updateConfig.php", "POST", { - header_title: newHeaderTitle, - oidc: newOIDCConfig, - disableFormLogin, - disableBasicAuth, - disableOIDCLogin, - enableWebDAV, - sharedMaxUploadSize, - globalOtpauthUrl - }, { "X-CSRF-Token": window.csrfToken }) - .then(response => { - if (response.success) { - showToast(t("settings_updated_successfully")); - localStorage.setItem("disableFormLogin", disableFormLogin); - localStorage.setItem("disableBasicAuth", disableBasicAuth); - localStorage.setItem("disableOIDCLogin", disableOIDCLogin); - localStorage.setItem("enableWebDAV", enableWebDAV); - localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize); - if (typeof window.updateLoginOptionsUI === "function") { - window.updateLoginOptionsUI({ - disableFormLogin, - disableBasicAuth, - disableOIDCLogin - }); - } - captureInitialAdminConfig(); - closeAdminPanel(); - loadAdminConfigFunc(); - } else { - showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error"))); - } - }) - .catch(() => { }); - }); - - // Enforce login option constraints. - const disableFormLoginCheckbox = document.getElementById("disableFormLogin"); - const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth"); - const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin"); - function enforceLoginOptionConstraint(changedCheckbox) { - const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox] - .filter(cb => cb.checked).length; - if (changedCheckbox.checked && totalDisabled === 3) { - showToast(t("at_least_one_login_method")); - changedCheckbox.checked = false; - } - } - disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); - disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); - disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); }); - - // Initial checkbox and input states - document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; - document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; - document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; - document.getElementById("enableWebDAV").checked = config.enableWebDAV === true; - document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || ""; - - captureInitialAdminConfig(); - - } else { - // Update existing modal and show - adminModal.style.backgroundColor = overlayBackground; - const modalContent = adminModal.querySelector(".modal-content"); - if (modalContent) { - modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff"; - modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000"; - modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc"; - } - document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl; - document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId; - document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret; - document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri; - document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'; - document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true; - document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true; - document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true; - document.getElementById("enableWebDAV").checked = config.enableWebDAV === true; - document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || ""; - adminModal.style.display = "flex"; - captureInitialAdminConfig(); - } - }) - .catch(() => { - let adminModal = document.getElementById("adminPanelModal"); - if (adminModal) { - adminModal.style.backgroundColor = "rgba(0,0,0,0.5)"; - const modalContent = adminModal.querySelector(".modal-content"); - if (modalContent) { - modalContent.style.background = "#fff"; - modalContent.style.color = "#000"; - modalContent.style.border = "1px solid #ccc"; - } - document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl; - document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId; - document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret; - document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri; - document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'; - document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true"; - document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true"; - document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true"; - document.getElementById("enableWebDAV").checked = localStorage.getItem("enableWebDAV") === "true"; - document.getElementById("sharedMaxUploadSize").value = localStorage.getItem("sharedMaxUploadSize") || ""; - adminModal.style.display = "flex"; - captureInitialAdminConfig(); - } else { - openAdminPanel(); - } - }); -} - -export async function closeAdminPanel() { - if (hasUnsavedChanges()) { - const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm")); - if (!userConfirmed) { - return; - } - } - const adminModal = document.getElementById("adminPanelModal"); - if (adminModal) adminModal.style.display = "none"; -} - -// --- New: User Permissions Modal --- -export function openUserPermissionsModal() { - let userPermissionsModal = document.getElementById("userPermissionsModal"); - const isDarkMode = document.body.classList.contains("dark-mode"); - const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; - const modalContentStyles = ` - background: ${isDarkMode ? "#2c2c2c" : "#fff"}; - color: ${isDarkMode ? "#e0e0e0" : "#000"}; - padding: 20px; - max-width: 500px; - width: 90%; - border-radius: 8px; - position: relative; - `; - - if (!userPermissionsModal) { - userPermissionsModal = document.createElement("div"); - userPermissionsModal.id = "userPermissionsModal"; - userPermissionsModal.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: ${overlayBackground}; - display: flex; - justify-content: center; - align-items: center; - z-index: 3500; - `; - userPermissionsModal.innerHTML = ` - - `; - document.body.appendChild(userPermissionsModal); - document.getElementById("closeUserPermissionsModal").addEventListener("click", () => { - userPermissionsModal.style.display = "none"; - }); - document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => { - userPermissionsModal.style.display = "none"; - }); - document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => { - // Collect permissions data from each user row. - const rows = userPermissionsModal.querySelectorAll(".user-permission-row"); - const permissionsData = []; - rows.forEach(row => { - const username = row.getAttribute("data-username"); - const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']"); - const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']"); - const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']"); - permissionsData.push({ - username, - folderOnly: folderOnlyCheckbox.checked, - readOnly: readOnlyCheckbox.checked, - disableUpload: disableUploadCheckbox.checked - }); - }); - // Send the permissionsData to the server. - sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken }) - .then(response => { - if (response.success) { - showToast(t("user_permissions_updated_successfully")); - userPermissionsModal.style.display = "none"; - } else { - showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error"))); - } - }) - .catch(() => { - showToast(t("error_updating_permissions")); - }); - }); - } else { - userPermissionsModal.style.display = "flex"; - } - // Load the list of users into the modal. - loadUserPermissionsList(); -} - -function loadUserPermissionsList() { - const listContainer = document.getElementById("userPermissionsList"); - if (!listContainer) return; - listContainer.innerHTML = ""; - - // First, fetch the current permissions from the server. - fetch("/api/getUserPermissions.php", { credentials: "include" }) - .then(response => response.json()) - .then(permissionsData => { - // Then, fetch the list of users. - return fetch("/api/getUsers.php", { credentials: "include" }) - .then(response => response.json()) - .then(usersData => { - const users = Array.isArray(usersData) ? usersData : (usersData.users || []); - if (users.length === 0) { - listContainer.innerHTML = "" + t("no_users_found") + "
"; - return; - } - users.forEach(user => { - // Skip admin users. - if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return; - - // Use stored permissions if available; otherwise fall back to defaults. - const defaultPerm = { - folderOnly: false, - readOnly: false, - disableUpload: false, - }; - - // Normalize the username key to match server storage (e.g., lowercase) - const usernameKey = user.username.toLowerCase(); - - const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData)) - ? permissionsData[usernameKey] - : defaultPerm; - - // Create a row for the user. - const row = document.createElement("div"); - row.classList.add("user-permission-row"); - row.setAttribute("data-username", user.username); - row.style.padding = "10px 0"; - row.innerHTML = ` -" + t("error_loading_users") + "
"; - }); } \ No newline at end of file diff --git a/public/js/i18n.js b/public/js/i18n.js index 070ad2d..112a72e 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -182,6 +182,20 @@ const translations = { "switch_to_light_mode": "Switch to light mode", "switch_to_dark_mode": "Switch to dark mode", + // Admin Panel + "header_settings": "Header Settings", + "shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)", + "max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads", + "manage_shared_links": "Manage Shared Links", + "folder_shares": "Folder Shares", + "file_shares": "File Shares", + "loading": "Loading…", + "error_loading_share_links": "Error loading share links", + "share_deleted_successfully": "Share deleted successfully", + "error_deleting_share": "Error deleting share", + "password_protected": "Password protected", + + // NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS: "admin_panel": "Admin Panel", "user_panel": "User Panel", diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php new file mode 100644 index 0000000..1e4bc9b --- /dev/null +++ b/src/controllers/AdminController.php @@ -0,0 +1,232 @@ + '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 existing settings + $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']) : ""; + + // ── NEW: enableWebDAV flag ────────────────────────────────────── + $enableWebDAV = false; + if (array_key_exists('enableWebDAV', $data)) { + $enableWebDAV = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN); + } elseif (isset($data['features']['enableWebDAV'])) { + $enableWebDAV = filter_var($data['features']['enableWebDAV'], FILTER_VALIDATE_BOOLEAN); + } + + // ── NEW: sharedMaxUploadSize ────────────────────────────────────── + $sharedMaxUploadSize = null; + if (array_key_exists('sharedMaxUploadSize', $data)) { + $sharedMaxUploadSize = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT); + } elseif (isset($data['features']['sharedMaxUploadSize'])) { + $sharedMaxUploadSize = filter_var($data['features']['sharedMaxUploadSize'], FILTER_VALIDATE_INT); + } + + $configUpdate = [ + 'header_title' => $headerTitle, + 'oidc' => [ + 'providerUrl' => $oidcProviderUrl, + 'clientId' => $oidcClientId, + 'clientSecret' => $oidcClientSecret, + 'redirectUri' => $oidcRedirectUri, + ], + 'loginOptions' => [ + 'disableFormLogin' => $disableFormLogin, + 'disableBasicAuth' => $disableBasicAuth, + 'disableOIDCLogin' => $disableOIDCLogin, + ], + 'globalOtpauthUrl' => $globalOtpauthUrl, + 'enableWebDAV' => $enableWebDAV, + 'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW + ]; + + // Delegate to the model. + $result = AdminModel::updateConfig($configUpdate); + if (isset($result['error'])) { + http_response_code(500); + } + echo json_encode($result); + exit; + } +} \ No newline at end of file diff --git a/src/controllers/AuthController.php b/src/controllers/AuthController.php new file mode 100644 index 0000000..5811cb8 --- /dev/null +++ b/src/controllers/AuthController.php @@ -0,0 +1,626 @@ +getMessage()); + http_response_code(500); + echo json_encode(['error' => 'Internal Server Error']); + exit(); + }); + + // Decode any JSON payload + $data = json_decode(file_get_contents('php://input'), true) ?: []; + $username = trim($data['username'] ?? ''); + $password = trim($data['password'] ?? ''); + $totpCode = trim($data['totp_code'] ?? ''); + $rememberMe = !empty($data['remember_me']); + + // + // 1) TOTP‑only step: user already passed credentials and we asked for TOTP, + // now they POST just totp_code. + // + if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) { + $username = $_SESSION['pending_login_user']; + $secret = $_SESSION['pending_login_secret']; + $rememberMe = $_SESSION['pending_login_remember_me'] ?? false; + $tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1); + if (! $tfa->verifyCode($secret, $totpCode)) { + echo json_encode(['error' => 'Invalid TOTP code']); + exit(); + } + // clear the pending markers + unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']); + // now finish login + $this->finalizeLogin($username, $rememberMe); + } + + // + // 2) OIDC flow + // + $oidcAction = $_GET['oidc'] ?? null; + if (! $oidcAction && isset($_GET['code'])) { + $oidcAction = 'callback'; + } + if ($oidcAction) { + $cfg = AdminModel::getConfig(); + $oidc = new OpenIDConnectClient( + $cfg['oidc']['providerUrl'], + $cfg['oidc']['clientId'], + $cfg['oidc']['clientSecret'] + ); + $oidc->setRedirectURL($cfg['oidc']['redirectUri']); + + if ($oidcAction === 'callback') { + try { + $oidc->authenticate(); + $username = $oidc->requestUserInfo('preferred_username'); + + // check if this user has a 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 && $parts[3] !== '') { + $totp_secret = decryptData($parts[3], $GLOBALS['encryptionKey']); + break; + } + } + } + if ($totp_secret) { + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $totp_secret; + header('Location: /index.html?totp_required=1'); + exit(); + } + + // no TOTP → finish immediately + $this->finishBrowserLogin($username); + } catch (\Exception $e) { + error_log("OIDC auth error: " . $e->getMessage()); + http_response_code(401); + echo json_encode(['error' => 'Authentication failed.']); + exit(); + } + } else { + // initial OIDC redirect + 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(); + } + } + } + + // + // 3) Form‑based / AJAX login + // + 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']); + exit(); + } + + // rate‑limit + $ip = $_SERVER['REMOTE_ADDR']; + $attemptsFile = USERS_DIR . 'failed_logins.json'; + $failed = AuthModel::loadFailedAttempts($attemptsFile); + if ( + isset($failed[$ip]) && + $failed[$ip]['count'] >= 5 && + time() - $failed[$ip]['last_attempt'] < 30 * 60 + ) { + 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) { + // record failure + $failed[$ip] = [ + 'count' => ($failed[$ip]['count'] ?? 0) + 1, + 'last_attempt' => time() + ]; + AuthModel::saveFailedAttempts($attemptsFile, $failed); + http_response_code(401); + echo json_encode(['error' => 'Invalid credentials']); + exit(); + } + + // if this account has TOTP, ask for it + if (! empty($user['totp_secret'])) { + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $user['totp_secret']; + $_SESSION['pending_login_remember_me'] = $rememberMe; + echo json_encode(['totp_required' => true]); + exit(); + } + + // otherwise clear rate‑limit & finish + if (isset($failed[$ip])) { + unset($failed[$ip]); + AuthModel::saveFailedAttempts($attemptsFile, $failed); + } + $this->finalizeLogin($username, $rememberMe); + } + + /** + * Finalize an AJAX‐style login (form/basic/TOTP) by + * issuing the session, remember‑me cookie, and JSON payload. + */ + protected function finalizeLogin(string $username, bool $rememberMe): void + { + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $username; + $_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1'); + + $perms = loadUserPermissions($username); + $_SESSION['folderOnly'] = $perms['folderOnly'] ?? false; + $_SESSION['readOnly'] = $perms['readOnly'] ?? false; + $_SESSION['disableUpload'] = $perms['disableUpload'] ?? false; + + // remember‑me + if ($rememberMe) { + $tokFile = USERS_DIR . 'persistent_tokens.json'; + $token = bin2hex(random_bytes(32)); + $expiry = time() + 30 * 24 * 60 * 60; + $all = []; + + if (file_exists($tokFile)) { + $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']); + $all = json_decode($dec, true) ?: []; + } + + $all[$token] = [ + 'username' => $username, + 'expiry' => $expiry, + 'isAdmin' => $_SESSION['isAdmin'] + ]; + + file_put_contents( + $tokFile, + encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']), + LOCK_EX + ); + + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + + setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true); + + setcookie( + session_name(), + session_id(), + $expiry, + '/', + '', + $secure, + true + ); + + session_regenerate_id(true); + } + + echo json_encode([ + 'status' => 'ok', + 'success' => 'Login successful', + 'isAdmin' => $_SESSION['isAdmin'], + 'folderOnly' => $_SESSION['folderOnly'], + 'readOnly' => $_SESSION['readOnly'], + 'disableUpload' => $_SESSION['disableUpload'], + 'username' => $username + ]); + exit(); + } + + /** + * A version of finalizeLogin() that ends in a browser redirect + * (used for OIDC non‑AJAX flows). + */ + protected function finishBrowserLogin(string $username): void + { + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $username; + $_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1'); + + $perms = loadUserPermissions($username); + $_SESSION['folderOnly'] = $perms['folderOnly'] ?? false; + $_SESSION['readOnly'] = $perms['readOnly'] ?? false; + $_SESSION['disableUpload'] = $perms['disableUpload'] ?? false; + + header('Location: /index.html'); + exit(); + } + + /** + * @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 + { + + // 1) Remember-me re-login + if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) { + $payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']); + if ($payload) { + $old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32)); + session_regenerate_id(true); + $_SESSION['csrf_token'] = $old; + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $payload['username']; + $_SESSION['isAdmin'] = !empty($payload['isAdmin']); + $_SESSION['folderOnly'] = $payload['folderOnly'] ?? false; + $_SESSION['readOnly'] = $payload['readOnly'] ?? false; + $_SESSION['disableUpload'] = $payload['disableUpload'] ?? false; + // regenerate CSRF if you use one + + + // TOTP enabled? (same logic as below) + $usersFile = USERS_DIR . USERS_FILE; + $totp = false; + if (file_exists($usersFile)) { + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) { + $totp = true; + break; + } + } + } + + echo json_encode([ + 'authenticated' => true, + 'csrf_token' => $_SESSION['csrf_token'], + 'isAdmin' => $_SESSION['isAdmin'], + 'totp_enabled' => $totp, + 'username' => $_SESSION['username'], + 'folderOnly' => $_SESSION['folderOnly'], + 'readOnly' => $_SESSION['readOnly'], + 'disableUpload' => $_SESSION['disableUpload'] + ]); + exit(); + } + } + + $usersFile = USERS_DIR . USERS_FILE; + + // 2) Setup mode? + if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') { + error_log("checkAuth: setup mode"); + echo json_encode(['setup' => true]); + exit(); + } + + // 3) Session-based auth + if (empty($_SESSION['authenticated'])) { + echo json_encode(['authenticated' => false]); + exit(); + } + + // 4) TOTP enabled? + $totp = false; + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) { + $totp = true; + break; + } + } + + // 5) Final response + $resp = [ + 'authenticated' => true, + 'isAdmin' => !empty($_SESSION['isAdmin']), + 'totp_enabled' => $totp, + 'username' => $_SESSION['username'], + 'folderOnly' => $_SESSION['folderOnly'] ?? false, + 'readOnly' => $_SESSION['readOnly'] ?? false, + 'disableUpload' => $_SESSION['disableUpload'] ?? false + ]; + + echo json_encode($resp); + 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 + { + // 1) Ensure session and CSRF token exist + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + // 2) Emit headers + header('Content-Type: application/json'); + header('X-CSRF-Token: ' . $_SESSION['csrf_token']); + + // 3) Return JSON payload + 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"); + // load _all_ the permissions + $userPerms = loadUserPermissions($username); + $_SESSION["folderOnly"] = $userPerms["folderOnly"] ?? false; + $_SESSION["readOnly"] = $userPerms["readOnly"] ?? false; + $_SESSION["disableUpload"] = $userPerms["disableUpload"] ?? false; + + 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; + } +} diff --git a/src/controllers/FileController.php b/src/controllers/FileController.php new file mode 100644 index 0000000..0c027b9 --- /dev/null +++ b/src/controllers/FileController.php @@ -0,0 +1,1604 @@ + "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"); +?> + + + + + + +This folder is protected by a password. Please enter the password to view its contents.
+ +