diff --git a/src/controllers/adminController.php b/src/controllers/adminController.php
deleted file mode 100644
index 1e4bc9b..0000000
--- a/src/controllers/adminController.php
+++ /dev/null
@@ -1,232 +0,0 @@
- '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
deleted file mode 100644
index 5811cb8..0000000
--- a/src/controllers/authController.php
+++ /dev/null
@@ -1,626 +0,0 @@
-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
deleted file mode 100644
index 0c027b9..0000000
--- a/src/controllers/fileController.php
+++ /dev/null
@@ -1,1604 +0,0 @@
- "Invalid CSRF token"]);
- exit;
- }
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // 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");
-?>
-
-
-
-
-
-
- Enter Password
-
-
-
-
- This file is protected by a password.
-
-
-
-
- "Invalid password."]);
- exit;
- }
- }
-
- // Build file path securely.
- $folder = trim($record['folder'], "/\\ ");
- $file = $record['file'];
- $filePath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
- if (!empty($folder) && strtolower($folder) !== 'root') {
- $filePath .= $folder . DIRECTORY_SEPARATOR;
- }
- $filePath .= $file;
-
- $realFilePath = realpath($filePath);
- $uploadDirReal = realpath(UPLOAD_DIR);
- if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
- http_response_code(404);
- header('Content-Type: application/json');
- echo json_encode(["error" => "File not found."]);
- exit;
- }
- if (!file_exists($realFilePath)) {
- http_response_code(404);
- header('Content-Type: application/json');
- echo json_encode(["error" => "File not found."]);
- exit;
- }
-
- // Serve the file.
- $mimeType = mime_content_type($realFilePath);
- header("Content-Type: " . $mimeType);
- $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
- if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'])) {
- header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
- } else {
- header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
- }
- header("Cache-Control: no-store, no-cache, must-revalidate");
- header("Pragma: no-cache");
- header('Content-Length: ' . filesize($realFilePath));
-
- readfile($realFilePath);
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/file/createShareLink.php",
- * summary="Create a share link for a file",
- * description="Generates a secure share link token for a specific file with optional password protection and a custom expiration time.",
- * operationId="createShareLink",
- * tags={"Files"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"folder", "file", "expirationValue", "expirationUnit"},
- * @OA\Property(property="folder", type="string", example="Documents"),
- * @OA\Property(property="file", type="string", example="report.pdf"),
- * @OA\Property(property="expirationValue", type="integer", example=1),
- * @OA\Property(
- * property="expirationUnit",
- * type="string",
- * enum={"seconds","minutes","hours","days"},
- * example="minutes"
- * ),
- * @OA\Property(property="password", type="string", example="secret")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Share link created successfully",
- * @OA\JsonContent(
- * @OA\Property(property="token", type="string", example="a1b2c3d4e5f6..."),
- * @OA\Property(property="expires", type="integer", example=1621234567)
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid request data"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Read-only users are not allowed to create share links"
- * )
- * )
- *
- * Creates a share link for a file.
- *
- * @return void Outputs JSON response.
- */
- public function createShareLink()
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Check user permissions.
- $username = $_SESSION['username'] ?? '';
- $userPermissions = loadUserPermissions($username);
- if ($username && !empty($userPermissions['readOnly'])) {
- http_response_code(403);
- echo json_encode(["error" => "Read-only users are not allowed to create share links."]);
- exit;
- }
-
- // Parse POST JSON input.
- $input = json_decode(file_get_contents("php://input"), true);
- if (!$input) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid input."]);
- exit;
- }
-
- // Extract parameters.
- $folder = isset($input['folder']) ? trim($input['folder']) : "";
- $file = isset($input['file']) ? basename($input['file']) : "";
- $value = isset($input['expirationValue']) ? intval($input['expirationValue']) : 60;
- $unit = isset($input['expirationUnit']) ? $input['expirationUnit'] : 'minutes';
- $password = isset($input['password']) ? $input['password'] : "";
-
- // Validate folder name.
- if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid folder name."]);
- exit;
- }
-
- // Convert the provided value+unit into seconds
- switch ($unit) {
- case 'seconds':
- $expirationSeconds = $value;
- break;
- case 'hours':
- $expirationSeconds = $value * 3600;
- break;
- case 'days':
- $expirationSeconds = $value * 86400;
- break;
- case 'minutes':
- default:
- $expirationSeconds = $value * 60;
- break;
- }
-
- // Delegate share link creation to the model.
- $result = FileModel::createShareLink($folder, $file, $expirationSeconds, $password);
-
- echo json_encode($result);
- }
-
- /**
- * @OA\Get(
- * path="/api/file/getTrashItems.php",
- * summary="Get trash items",
- * description="Retrieves a list of files that have been moved to Trash, enriched with metadata such as who deleted them and when.",
- * operationId="getTrashItems",
- * tags={"Files"},
- * @OA\Response(
- * response=200,
- * description="Trash items retrieved successfully",
- * @OA\JsonContent(type="array", @OA\Items(type="object"))
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * )
- * )
- *
- * Retrieves trash items from the trash metadata file.
- *
- * @return void Outputs JSON response with trash items.
- */
- public function getTrashItems()
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Delegate to the model.
- $trashItems = FileModel::getTrashItems();
- echo json_encode($trashItems);
- }
-
- /**
- * @OA\Post(
- * path="/api/file/restoreFiles.php",
- * summary="Restore trashed files",
- * description="Restores files from Trash based on provided trash file identifiers and updates metadata.",
- * operationId="restoreFiles",
- * tags={"Files"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"files"},
- * @OA\Property(property="files", type="array", @OA\Items(type="string", example="trashedFile_1623456789.zip"))
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Files restored successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="Items restored: file1, file2"),
- * @OA\Property(property="restored", type="array", @OA\Items(type="string"))
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid request"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * )
- * )
- *
- * Restores files from Trash based on provided trash file names.
- *
- * @return void Outputs JSON response.
- */
- public function restoreFiles()
- {
- header('Content-Type: application/json');
-
- // CSRF Protection.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Read POST input.
- $data = json_decode(file_get_contents("php://input"), true);
- if (!isset($data['files']) || !is_array($data['files'])) {
- http_response_code(400);
- echo json_encode(["error" => "No file or folder identifiers provided"]);
- exit;
- }
-
- // Delegate restoration to the model.
- $result = FileModel::restoreFiles($data['files']);
- echo json_encode($result);
- }
-
- /**
- * @OA\Post(
- * path="/api/file/deleteTrashFiles.php",
- * summary="Delete trash files",
- * description="Deletes trash items based on provided trash file identifiers from the trash metadata and removes the files from disk.",
- * operationId="deleteTrashFiles",
- * tags={"Files"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * oneOf={
- * @OA\Schema(
- * required={"deleteAll"},
- * @OA\Property(property="deleteAll", type="boolean", example=true)
- * ),
- * @OA\Schema(
- * required={"files"},
- * @OA\Property(
- * property="files",
- * type="array",
- * @OA\Items(type="string", example="trashedfile_1234567890")
- * )
- * )
- * }
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Trash items deleted successfully",
- * @OA\JsonContent(
- * @OA\Property(property="deleted", type="array", @OA\Items(type="string"))
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid input"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * )
- * )
- *
- * Deletes trash files by processing provided trash file identifiers.
- *
- * @return void Outputs a JSON response.
- */
- public function deleteTrashFiles()
- {
- header('Content-Type: application/json');
-
- // CSRF Protection.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Read and decode JSON input.
- $data = json_decode(file_get_contents("php://input"), true);
- if (!$data) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid input"]);
- exit;
- }
-
- // Determine deletion mode.
- $filesToDelete = [];
- if (isset($data['deleteAll']) && $data['deleteAll'] === true) {
- // In this case, we need to delete all trash items.
- // Load current trash metadata.
- $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
- $shareFile = $trashDir . "trash.json";
- if (file_exists($shareFile)) {
- $json = file_get_contents($shareFile);
- $tempData = json_decode($json, true);
- if (is_array($tempData)) {
- foreach ($tempData as $item) {
- if (isset($item['trashName'])) {
- $filesToDelete[] = $item['trashName'];
- }
- }
- }
- }
- } elseif (isset($data['files']) && is_array($data['files'])) {
- $filesToDelete = $data['files'];
- } else {
- http_response_code(400);
- echo json_encode(["error" => "No trash file identifiers provided"]);
- exit;
- }
-
- // Delegate deletion to the model.
- $result = FileModel::deleteTrashFiles($filesToDelete);
-
- // Build a human‑friendly success or error message
- if (!empty($result['deleted'])) {
- $count = count($result['deleted']);
- $msg = "Trash item" . ($count === 1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
- echo json_encode(["success" => $msg]);
- } elseif (!empty($result['error'])) {
- echo json_encode(["error" => $result['error']]);
- } else {
- echo json_encode(["success" => "No items to delete."]);
- }
- exit;
- }
-
- /**
- * @OA\Get(
- * path="/api/file/getFileTag.php",
- * summary="Retrieve file tags",
- * description="Retrieves tags from the createdTags.json metadata file.",
- * operationId="getFileTags",
- * tags={"Files"},
- * @OA\Response(
- * response=200,
- * description="File tags retrieved successfully",
- * @OA\JsonContent(
- * type="array",
- * @OA\Items(type="object")
- * )
- * )
- * )
- *
- * Retrieves file tags from the createdTags.json metadata file.
- *
- * @return void Outputs JSON response with file tags.
- */
- public function getFileTags(): void
- {
- header('Content-Type: application/json; charset=utf-8');
-
- $tags = FileModel::getFileTags();
- echo json_encode($tags);
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/file/saveFileTag.php",
- * summary="Save file tags",
- * description="Saves tag data for a specified file and updates global tag data. For folder-specific tags, saves to the folder's metadata file.",
- * operationId="saveFileTag",
- * tags={"Files"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"file", "tags"},
- * @OA\Property(property="file", type="string", example="document.txt"),
- * @OA\Property(property="folder", type="string", example="Documents"),
- * @OA\Property(
- * property="tags",
- * type="array",
- * @OA\Items(
- * type="object",
- * @OA\Property(property="name", type="string", example="Important"),
- * @OA\Property(property="color", type="string", example="#FF0000")
- * )
- * ),
- * @OA\Property(property="deleteGlobal", type="boolean", example=false),
- * @OA\Property(property="tagToDelete", type="string", example="OldTag")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Tag data saved successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="Tag data saved successfully."),
- * @OA\Property(property="globalTags", type="array", @OA\Items(type="object"))
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid request data"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token or insufficient permissions"
- * )
- * )
- *
- * Saves tag data for a file and updates the global tag repository.
- *
- * @return void Outputs JSON response.
- */
- public function saveFileTag(): void
- {
- header("Cache-Control: no-cache, no-store, must-revalidate");
- header("Pragma: no-cache");
- header("Expires: 0");
- header('Content-Type: application/json');
-
- // CSRF Protection.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfHeader = $headersArr['x-csrf-token'] ?? '';
- if (!isset($_SESSION['csrf_token']) || trim($csrfHeader) !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Check that the user is not read-only.
- $username = $_SESSION['username'] ?? '';
- $userPermissions = loadUserPermissions($username);
- if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
- echo json_encode(["error" => "Read-only users are not allowed to file tags"]);
- exit;
- }
-
- // Retrieve and sanitize input.
- $data = json_decode(file_get_contents('php://input'), true);
- if (!$data) {
- http_response_code(400);
- echo json_encode(["error" => "No data received"]);
- exit;
- }
-
- $file = isset($data['file']) ? trim($data['file']) : '';
- $folder = isset($data['folder']) ? trim($data['folder']) : 'root';
- $tags = $data['tags'] ?? [];
- $deleteGlobal = isset($data['deleteGlobal']) ? (bool)$data['deleteGlobal'] : false;
- $tagToDelete = isset($data['tagToDelete']) ? trim($data['tagToDelete']) : null;
-
- if ($file === '') {
- http_response_code(400);
- echo json_encode(["error" => "No file specified."]);
- exit;
- }
-
- // Validate folder name.
- if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid folder name."]);
- exit;
- }
-
- // Delegate to the model.
- $result = FileModel::saveFileTag($folder, $file, $tags, $deleteGlobal, $tagToDelete);
- echo json_encode($result);
- }
-
- /**
- * @OA\Get(
- * path="/api/file/getFileList.php",
- * summary="Get file list",
- * description="Retrieves a list of files from a specified folder along with global tags and metadata.",
- * operationId="getFileList",
- * tags={"Files"},
- * @OA\Parameter(
- * name="folder",
- * in="query",
- * description="Folder name (defaults to 'root')",
- * required=false,
- * @OA\Schema(type="string", example="Documents")
- * ),
- * @OA\Response(
- * response=200,
- * description="File list retrieved successfully",
- * @OA\JsonContent(
- * type="object",
- * @OA\Property(property="files", type="array", @OA\Items(type="object")),
- * @OA\Property(property="globalTags", type="array", @OA\Items(type="object"))
- * )
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * )
- * )
- *
- * Retrieves the file list and associated metadata for the specified folder.
- *
- * @return void Outputs JSON response.
- */
- public function getFileList(): void
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Retrieve the folder from GET; default to "root".
- $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
- if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid folder name."]);
- exit;
- }
-
- // Delegate to the model.
- $result = FileModel::getFileList($folder);
- if (isset($result['error'])) {
- http_response_code(400);
- }
- echo json_encode($result);
- exit;
- }
-
- /**
- * GET /api/file/getShareLinks.php
- */
- public function getShareLinks()
- {
- header('Content-Type: application/json');
- $shareFile = FileModel::getAllShareLinks();
- echo json_encode($shareFile, JSON_PRETTY_PRINT);
- }
-
- /**
- * POST /api/file/deleteShareLink.php
- */
- public function deleteShareLink()
- {
- header('Content-Type: application/json');
- $token = $_POST['token'] ?? '';
- if (!$token) {
- echo json_encode(['success' => false, 'error' => 'No token provided']);
- return;
- }
-
- $deleted = FileModel::deleteShareLink($token);
- if ($deleted) {
- echo json_encode(['success' => true]);
- } else {
- echo json_encode(['success' => false, 'error' => 'Not found']);
- }
- }
-}
diff --git a/src/controllers/folderController.php b/src/controllers/folderController.php
deleted file mode 100644
index da99157..0000000
--- a/src/controllers/folderController.php
+++ /dev/null
@@ -1,1107 +0,0 @@
- "Unauthorized"]);
- exit;
- }
-
- // Ensure the request method is POST.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- echo json_encode(['error' => 'Invalid request method.']);
- exit;
- }
-
- // CSRF check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = $headersArr['x-csrf-token'] ?? '';
- if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(['error' => 'Invalid CSRF token.']);
- exit;
- }
-
- // Check permissions.
- $username = $_SESSION['username'] ?? '';
- $userPermissions = loadUserPermissions($username);
- if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
- echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
- exit;
- }
-
- // Get and decode JSON input.
- $input = json_decode(file_get_contents('php://input'), true);
- if (!isset($input['folderName'])) {
- echo json_encode(['error' => 'Folder name not provided.']);
- exit;
- }
-
- $folderName = trim($input['folderName']);
- $parent = isset($input['parent']) ? trim($input['parent']) : "";
-
- // Basic sanitation for folderName.
- if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
- echo json_encode(['error' => 'Invalid folder name.']);
- exit;
- }
-
- // Optionally sanitize the parent.
- if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
- echo json_encode(['error' => 'Invalid parent folder name.']);
- exit;
- }
-
- // Delegate to FolderModel.
- $result = FolderModel::createFolder($folderName, $parent);
- echo json_encode($result);
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/folder/deleteFolder.php",
- * summary="Delete an empty folder",
- * description="Deletes a specified folder if it is empty and not the root folder, and also removes its metadata file.",
- * operationId="deleteFolder",
- * tags={"Folders"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"folder"},
- * @OA\Property(property="folder", type="string", example="Documents/Subfolder")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Folder deleted successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="boolean", example=true)
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request (e.g., invalid folder name or folder not empty)"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token or permission denied"
- * )
- * )
- *
- * Deletes a folder if it is empty and not the root folder.
- *
- * @return void Outputs a JSON response.
- */
- public function deleteFolder(): void
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Ensure the request is a POST.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- echo json_encode(["error" => "Invalid request method."]);
- exit;
- }
-
- // CSRF Protection.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token."]);
- exit;
- }
-
- // Check user permissions.
- $username = $_SESSION['username'] ?? '';
- $userPermissions = loadUserPermissions($username);
- if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
- echo json_encode(["error" => "Read-only users are not allowed to delete folders."]);
- exit;
- }
-
- // Get and decode JSON input.
- $input = json_decode(file_get_contents('php://input'), true);
- if (!isset($input['folder'])) {
- echo json_encode(["error" => "Folder name not provided."]);
- exit;
- }
-
- $folder = trim($input['folder']);
- // Prevent deletion of the root folder.
- if (strtolower($folder) === 'root') {
- echo json_encode(["error" => "Cannot delete root folder."]);
- exit;
- }
-
- // Delegate to the model.
- $result = FolderModel::deleteFolder($folder);
- echo json_encode($result);
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/folder/renameFolder.php",
- * summary="Rename a folder",
- * description="Renames an existing folder and updates its associated metadata files.",
- * operationId="renameFolder",
- * tags={"Folders"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"oldFolder", "newFolder"},
- * @OA\Property(property="oldFolder", type="string", example="Documents/OldFolder"),
- * @OA\Property(property="newFolder", type="string", example="Documents/NewFolder")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Folder renamed successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="boolean", example=true)
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid folder names or folder does not exist"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token or permission denied"
- * )
- * )
- *
- * Renames a folder by validating inputs and delegating to the model.
- *
- * @return void Outputs a JSON response.
- */
- public function renameFolder(): void
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Ensure the request method is POST.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- echo json_encode(['error' => 'Invalid request method.']);
- exit;
- }
-
- // CSRF Protection.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token."]);
- exit;
- }
-
- // Check that the user is not read-only.
- $username = $_SESSION['username'] ?? '';
- $userPermissions = loadUserPermissions($username);
- if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
- echo json_encode(["error" => "Read-only users are not allowed to rename folders."]);
- exit;
- }
-
- // Get JSON input.
- $input = json_decode(file_get_contents('php://input'), true);
- if (!isset($input['oldFolder']) || !isset($input['newFolder'])) {
- echo json_encode(['error' => 'Required folder names not provided.']);
- exit;
- }
-
- $oldFolder = trim($input['oldFolder']);
- $newFolder = trim($input['newFolder']);
-
- // Validate folder names.
- if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
- echo json_encode(['error' => 'Invalid folder name(s).']);
- exit;
- }
-
- // Delegate to the model.
- $result = FolderModel::renameFolder($oldFolder, $newFolder);
- echo json_encode($result);
- exit;
- }
-
- /**
- * @OA\Get(
- * path="/api/folder/getFolderList.php",
- * summary="Get list of folders",
- * description="Retrieves the list of folders in the upload directory, including file counts and metadata file names for each folder.",
- * operationId="getFolderList",
- * tags={"Folders"},
- * @OA\Parameter(
- * name="folder",
- * in="query",
- * description="Optional folder name to filter the listing",
- * required=false,
- * @OA\Schema(type="string", example="Documents")
- * ),
- * @OA\Response(
- * response=200,
- * description="Folder list retrieved successfully",
- * @OA\JsonContent(
- * type="array",
- * @OA\Items(type="object")
- * )
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad request"
- * )
- * )
- *
- * Retrieves the folder list and associated metadata.
- *
- * @return void Outputs JSON response.
- */
- public function getFolderList(): void
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Optionally, you might add further input validation if necessary.
- $folderList = FolderModel::getFolderList();
- echo json_encode($folderList);
- exit;
- }
-
- /**
- * @OA\Get(
- * path="/api/folder/shareFolder.php",
- * summary="Display a shared folder",
- * description="Renders an HTML view of a shared folder's contents. Supports password protection, file listing with pagination, and an upload container if uploads are allowed.",
- * operationId="shareFolder",
- * tags={"Folders"},
- * @OA\Parameter(
- * name="token",
- * in="query",
- * description="The share token for the folder",
- * required=true,
- * @OA\Schema(type="string")
- * ),
- * @OA\Parameter(
- * name="pass",
- * in="query",
- * description="The password if the folder is protected",
- * required=false,
- * @OA\Schema(type="string")
- * ),
- * @OA\Parameter(
- * name="page",
- * in="query",
- * description="Page number for pagination",
- * required=false,
- * @OA\Schema(type="integer", example=1)
- * ),
- * @OA\Response(
- * response=200,
- * description="Shared folder displayed",
- * @OA\MediaType(mediaType="text/html")
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid request"
- * ),
- * @OA\Response(
- * response=403,
- * description="Access forbidden (expired link or invalid password)"
- * ),
- * @OA\Response(
- * response=404,
- * description="Share folder not found"
- * )
- * )
- *
- * Displays a shared folder with file listings, pagination, and an upload container if allowed.
- *
- * @return void Outputs HTML content.
- */
-
- function formatBytes($bytes)
- {
- if ($bytes < 1024) {
- return $bytes . " B";
- } elseif ($bytes < 1024 * 1024) {
- return round($bytes / 1024, 2) . " KB";
- } elseif ($bytes < 1024 * 1024 * 1024) {
- return round($bytes / (1024 * 1024), 2) . " MB";
- } else {
- return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
- }
- }
-
- public function shareFolder(): void
- {
- // Retrieve GET parameters.
- $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
- $providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
- $page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
- if ($page === false || $page < 1) {
- $page = 1;
- }
-
- if (empty($token)) {
- http_response_code(400);
- header('Content-Type: application/json');
- echo json_encode(["error" => "Missing token."]);
- exit;
- }
-
- // Delegate to the model.
- $data = FolderModel::getSharedFolderData($token, $providedPass, $page);
-
- // If a password is needed, output an HTML form.
- if (isset($data['needs_password']) && $data['needs_password'] === true) {
- header("Content-Type: text/html; charset=utf-8");
-?>
-
-
-
-
-
-
- Enter Password
-
-
-
-
-
-
Folder Protected
-
This folder is protected by a password. Please enter the password to view its contents.
-
-
-
-
-
- $data['error']]);
- exit;
- }
-
- // Load admin config so we can pull the sharedMaxUploadSize
- require_once PROJECT_ROOT . '/src/models/AdminModel.php';
- $adminConfig = AdminModel::getConfig();
- $sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
- ? (int)$adminConfig['sharedMaxUploadSize']
- : null;
-
- // For human‐readable formatting
- function formatBytes($bytes)
- {
- if ($bytes < 1024) {
- return $bytes . " B";
- } elseif ($bytes < 1024 * 1024) {
- return round($bytes / 1024, 2) . " KB";
- } elseif ($bytes < 1024 * 1024 * 1024) {
- return round($bytes / (1024 * 1024), 2) . " MB";
- } else {
- return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
- }
- }
-
- // Extract data for the HTML view.
- $folderName = $data['folder'];
- $files = $data['files'];
- $currentPage = $data['currentPage'];
- $totalPages = $data['totalPages'];
-
- // Build the HTML view.
- header("Content-Type: text/html; charset=utf-8");
- ?>
-
-
-
-
-
- Shared Folder:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This folder is empty.
-
-
-
-
- | Filename |
- Size |
-
-
-
-
-
- |
-
-
- ⇩
-
- |
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Upload File
-
- ( max size)
-
-
-
-
-
-
-
-
-
-
-
-
-
- "Unauthorized"]);
- exit;
- }
-
- // Read-only check
- $username = $_SESSION['username'] ?? '';
- $perms = loadUserPermissions($username);
- if ($username && !empty($perms['readOnly'])) {
- http_response_code(403);
- echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
- exit;
- }
-
- // Input
- $in = json_decode(file_get_contents("php://input"), true);
- if (!$in || !isset($in['folder'])) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid input."]);
- exit;
- }
-
- $folder = trim($in['folder']);
- $value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
- $unit = $in['expirationUnit'] ?? 'minutes';
- $password = $in['password'] ?? '';
- $allowUpload = intval($in['allowUpload'] ?? 0);
-
- // Folder name validation
- if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid folder name."]);
- exit;
- }
-
- // Convert to seconds
- switch ($unit) {
- case 'seconds':
- $seconds = $value;
- break;
- case 'hours':
- $seconds = $value * 3600;
- break;
- case 'days':
- $seconds = $value * 86400;
- break;
- case 'minutes':
- default:
- $seconds = $value * 60;
- break;
- }
-
- // Delegate
- $res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload);
- echo json_encode($res);
- exit;
- }
-
- /**
- * @OA\Get(
- * path="/api/folder/downloadSharedFile.php",
- * summary="Download a file from a shared folder",
- * description="Retrieves and serves a file from a shared folder based on a share token.",
- * operationId="downloadSharedFile",
- * tags={"Folders"},
- * @OA\Parameter(
- * name="token",
- * in="query",
- * description="The share folder token",
- * required=true,
- * @OA\Schema(type="string")
- * ),
- * @OA\Parameter(
- * name="file",
- * in="query",
- * description="The filename to download",
- * required=true,
- * @OA\Schema(type="string")
- * ),
- * @OA\Response(
- * response=200,
- * description="File served successfully",
- * @OA\MediaType(mediaType="application/octet-stream")
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request (missing parameters, invalid file name, etc.)"
- * ),
- * @OA\Response(
- * response=403,
- * description="Access forbidden (e.g., expired share link)"
- * ),
- * @OA\Response(
- * response=404,
- * description="File not found"
- * )
- * )
- *
- * Downloads a file from a shared folder based on a token.
- *
- * @return void Outputs the file with proper headers.
- */
- public function downloadSharedFile(): void
- {
- // Retrieve and sanitize GET parameters.
- $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
- $file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING);
-
- if (empty($token) || empty($file)) {
- http_response_code(400);
- header('Content-Type: application/json');
- echo json_encode(["error" => "Missing token or file parameter."]);
- exit;
- }
-
- // Delegate to the model.
- $result = FolderModel::getSharedFileInfo($token, $file);
- if (isset($result['error'])) {
- http_response_code(404);
- header('Content-Type: application/json');
- echo json_encode(["error" => $result['error']]);
- exit;
- }
-
- $realFilePath = $result['realFilePath'];
- $mimeType = $result['mimeType'];
-
- // Serve the file.
- header("Content-Type: " . $mimeType);
- $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
- if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'])) {
- header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
- } else {
- header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
- }
- header('Content-Length: ' . filesize($realFilePath));
- readfile($realFilePath);
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/folder/uploadToSharedFolder.php",
- * summary="Upload a file to a shared folder",
- * description="Handles file upload to a shared folder using a share token. Validates file size, extension, and uploads the file to the shared folder, updating metadata accordingly.",
- * operationId="uploadToSharedFolder",
- * tags={"Folders"},
- * @OA\RequestBody(
- * required=true,
- * description="Multipart form data containing the share token and file to upload.",
- * @OA\MediaType(
- * mediaType="multipart/form-data",
- * @OA\Schema(
- * required={"token", "fileToUpload"},
- * @OA\Property(property="token", type="string"),
- * @OA\Property(property="fileToUpload", type="string", format="binary")
- * )
- * )
- * ),
- * @OA\Response(
- * response=302,
- * description="Redirects to the shared folder page on success."
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request (missing token, file upload error, file type/size not allowed)"
- * ),
- * @OA\Response(
- * response=403,
- * description="Forbidden (share link expired or uploads not allowed)"
- * ),
- * @OA\Response(
- * response=500,
- * description="Server error during file move"
- * )
- * )
- *
- * Handles uploading a file to a shared folder.
- *
- * @return void Redirects upon successful upload or outputs JSON errors.
- */
- public function uploadToSharedFolder(): void
- {
- // Ensure request is POST.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- http_response_code(405);
- header('Content-Type: application/json');
- echo json_encode(["error" => "Method not allowed."]);
- exit;
- }
-
- // Ensure the share token is provided.
- if (empty($_POST['token'])) {
- http_response_code(400);
- header('Content-Type: application/json');
- echo json_encode(["error" => "Missing share token."]);
- exit;
- }
- $token = trim($_POST['token']);
-
- // Delegate the upload to the model.
- if (!isset($_FILES['fileToUpload'])) {
- http_response_code(400);
- header('Content-Type: application/json');
- echo json_encode(["error" => "No file was uploaded."]);
- exit;
- }
- $fileUpload = $_FILES['fileToUpload'];
-
- $result = FolderModel::uploadToSharedFolder($token, $fileUpload);
- if (isset($result['error'])) {
- http_response_code(400);
- header('Content-Type: application/json');
- echo json_encode($result);
- exit;
- }
-
- // Optionally, set a flash message in session.
- $_SESSION['upload_message'] = "File uploaded successfully.";
-
- // Redirect back to the shared folder view.
- $redirectUrl = "/api/folder/shareFolder.php?token=" . urlencode($token);
- header("Location: " . $redirectUrl);
- exit;
- }
-
- /**
- * GET /api/folder/getShareFolderLinks.php
- */
- public function getShareFolderLinks()
- {
- header('Content-Type: application/json');
- $links = FolderModel::getAllShareFolderLinks();
- echo json_encode($links, JSON_PRETTY_PRINT);
- }
-
- /**
- * POST /api/folder/deleteShareFolderLink.php
- */
- public function deleteShareFolderLink()
- {
- header('Content-Type: application/json');
- $token = $_POST['token'] ?? '';
- if (!$token) {
- echo json_encode(['success' => false, 'error' => 'No token provided']);
- return;
- }
-
- $deleted = FolderModel::deleteShareFolderLink($token);
- if ($deleted) {
- echo json_encode(['success' => true]);
- } else {
- echo json_encode(['success' => false, 'error' => 'Not found']);
- }
- }
-}
diff --git a/src/controllers/uploadController.php b/src/controllers/uploadController.php
deleted file mode 100644
index 273b7b2..0000000
--- a/src/controllers/uploadController.php
+++ /dev/null
@@ -1,199 +0,0 @@
- true,
- 'csrf_token' => $_SESSION['csrf_token']
- ]);
- exit;
- }
-
- //
- // 2) Auth checks
- //
- if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
- $userPerms = loadUserPermissions($_SESSION['username']);
- if (!empty($userPerms['disableUpload'])) {
- http_response_code(403);
- echo json_encode(["error" => "Upload disabled for this user."]);
- exit;
- }
-
- //
- // 3) Delegate the actual file handling
- //
- $result = UploadModel::handleUpload($_POST, $_FILES);
-
- //
- // 4) Respond
- //
- if (isset($result['error'])) {
- http_response_code(400);
- echo json_encode($result);
- exit;
- }
- if (isset($result['status'])) {
- echo json_encode($result);
- exit;
- }
-
- // full‐upload redirect
- $_SESSION['upload_message'] = "File uploaded successfully.";
- exit;
- }
-
- /**
- * @OA\Post(
- * path="/api/upload/removeChunks.php",
- * summary="Remove chunked upload temporary directory",
- * description="Removes the temporary directory used for chunked uploads, given a folder name matching the expected resumable pattern.",
- * operationId="removeChunks",
- * tags={"Uploads"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"folder"},
- * @OA\Property(property="folder", type="string", example="resumable_myupload123")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Temporary folder removed successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="boolean", example=true),
- * @OA\Property(property="message", type="string", example="Temporary folder removed.")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid input (e.g., missing folder or invalid folder name)"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * )
- * )
- *
- * Removes the temporary upload folder for chunked uploads.
- *
- * @return void Outputs a JSON response.
- */
- public function removeChunks(): void {
- header('Content-Type: application/json');
-
- // CSRF Protection: Validate token from POST data.
- $receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : '';
- if ($receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Check that the folder parameter is provided.
- if (!isset($_POST['folder'])) {
- http_response_code(400);
- echo json_encode(["error" => "No folder specified"]);
- exit;
- }
-
- $folder = $_POST['folder'];
- $result = UploadModel::removeChunks($folder);
- echo json_encode($result);
- exit;
- }
-}
\ No newline at end of file
diff --git a/src/controllers/userController.php b/src/controllers/userController.php
deleted file mode 100644
index 89e2b8d..0000000
--- a/src/controllers/userController.php
+++ /dev/null
@@ -1,1014 +0,0 @@
- "Unauthorized"]);
- exit;
- }
-
- // Retrieve users using the model
- $users = userModel::getAllUsers();
- echo json_encode($users);
- }
-
- /**
- * @OA\Post(
- * path="/api/addUser.php",
- * summary="Add a new user",
- * description="Adds a new user to the system. In setup mode, the new user is automatically made admin.",
- * operationId="addUser",
- * tags={"Users"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"username", "password"},
- * @OA\Property(property="username", type="string", example="johndoe"),
- * @OA\Property(property="password", type="string", example="securepassword"),
- * @OA\Property(property="isAdmin", type="boolean", example=true)
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="User added successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="User added successfully")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * )
- * )
- */
-
- public function addUser()
- {
- // 1) Ensure JSON output and session
- header('Content-Type: application/json');
-
- // 1a) Initialize CSRF token if missing
- if (empty($_SESSION['csrf_token'])) {
- $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
- }
-
- // 2) Determine setup mode (first-ever admin creation)
- $usersFile = USERS_DIR . USERS_FILE;
- $isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
- $setupMode = false;
- if (
- $isSetup && (! file_exists($usersFile)
- || filesize($usersFile) === 0
- || trim(file_get_contents($usersFile)) === ''
- )
- ) {
- $setupMode = true;
- } else {
- // 3) In non-setup, enforce CSRF + auth checks
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = trim($headersArr['x-csrf-token'] ?? '');
-
- // 3a) Soft-fail CSRF: on mismatch, regenerate and return new token
- if ($receivedToken !== $_SESSION['csrf_token']) {
- $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
- header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
- echo json_encode([
- 'csrf_expired' => true,
- 'csrf_token' => $_SESSION['csrf_token']
- ]);
- exit;
- }
-
- // 3b) Must be logged in as admin
- if (
- empty($_SESSION['authenticated'])
- || $_SESSION['authenticated'] !== true
- || empty($_SESSION['isAdmin'])
- || $_SESSION['isAdmin'] !== true
- ) {
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
- }
-
- // 4) Parse input
- $data = json_decode(file_get_contents('php://input'), true) ?: [];
- $newUsername = trim($data['username'] ?? '');
- $newPassword = trim($data['password'] ?? '');
-
- // 5) Determine admin flag
- if ($setupMode) {
- $isAdmin = '1';
- } else {
- $isAdmin = !empty($data['isAdmin']) ? '1' : '0';
- }
-
- // 6) Validate fields
- if ($newUsername === '' || $newPassword === '') {
- echo json_encode(["error" => "Username and password required"]);
- exit;
- }
- if (!preg_match(REGEX_USER, $newUsername)) {
- echo json_encode([
- "error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."
- ]);
- exit;
- }
-
- // 7) Delegate to model
- $result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
-
- // 8) Return model result
- echo json_encode($result);
- exit;
- }
-
- /**
- * @OA\Delete(
- * path="/api/removeUser.php",
- * summary="Remove a user",
- * description="Removes the specified user from the system. Cannot remove the currently logged-in user.",
- * operationId="removeUser",
- * tags={"Users"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"username"},
- * @OA\Property(property="username", type="string", example="johndoe")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="User removed successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="User removed successfully")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * )
- * )
- */
-
- public function removeUser()
- {
- header('Content-Type: application/json');
-
- // CSRF token check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Authentication and admin check.
- if (
- !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
- !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
- ) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Retrieve JSON data.
- $data = json_decode(file_get_contents("php://input"), true);
- $usernameToRemove = trim($data["username"] ?? "");
-
- if (!$usernameToRemove) {
- echo json_encode(["error" => "Username is required"]);
- exit;
- }
-
- // Validate the username format.
- if (!preg_match(REGEX_USER, $usernameToRemove)) {
- echo json_encode(["error" => "Invalid username format"]);
- exit;
- }
-
- // Prevent removal of the currently logged-in user.
- if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) {
- echo json_encode(["error" => "Cannot remove yourself"]);
- exit;
- }
-
- // Delegate the removal logic to the model.
- $result = userModel::removeUser($usernameToRemove);
- echo json_encode($result);
- }
-
- /**
- * @OA\Get(
- * path="/api/getUserPermissions.php",
- * summary="Retrieve user permissions",
- * description="Returns the permissions for the current user, or all permissions if the user is an admin.",
- * operationId="getUserPermissions",
- * tags={"Users"},
- * @OA\Response(
- * response=200,
- * description="Successful response with user permissions",
- * @OA\JsonContent(type="object")
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * )
- * )
- */
-
- public function getUserPermissions()
- {
- header('Content-Type: application/json');
-
- // Check if the user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Delegate to the model.
- $permissions = userModel::getUserPermissions();
- echo json_encode($permissions);
- }
-
- /**
- * @OA\Put(
- * path="/api/updateUserPermissions.php",
- * summary="Update user permissions",
- * description="Updates permissions for users. Only available to authenticated admin users.",
- * operationId="updateUserPermissions",
- * tags={"Users"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"permissions"},
- * @OA\Property(
- * property="permissions",
- * type="array",
- * @OA\Items(
- * type="object",
- * @OA\Property(property="username", type="string", example="johndoe"),
- * @OA\Property(property="folderOnly", type="boolean", example=true),
- * @OA\Property(property="readOnly", type="boolean", example=false),
- * @OA\Property(property="disableUpload", type="boolean", example=false)
- * )
- * )
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="User permissions updated successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="User permissions updated successfully.")
- * )
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * )
- * )
- */
-
- public function updateUserPermissions()
- {
- header('Content-Type: application/json');
-
- // Only admins can update permissions.
- if (
- !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
- !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
- ) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Verify CSRF token from headers.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Get POST input.
- $input = json_decode(file_get_contents("php://input"), true);
- if (!isset($input['permissions']) || !is_array($input['permissions'])) {
- echo json_encode(["error" => "Invalid input"]);
- exit;
- }
-
- $permissions = $input['permissions'];
-
- // Delegate to the model.
- $result = userModel::updateUserPermissions($permissions);
- echo json_encode($result);
- }
-
- /**
- * @OA\Post(
- * path="/api/changePassword.php",
- * summary="Change user password",
- * description="Allows an authenticated user to change their password by verifying the old password and updating to a new one.",
- * operationId="changePassword",
- * tags={"Users"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"oldPassword", "newPassword", "confirmPassword"},
- * @OA\Property(property="oldPassword", type="string", example="oldpass123"),
- * @OA\Property(property="newPassword", type="string", example="newpass456"),
- * @OA\Property(property="confirmPassword", type="string", example="newpass456")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Password updated successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="Password updated successfully.")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * )
- * )
- */
-
- public function changePassword()
- {
- header('Content-Type: application/json');
-
- // Ensure user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(401);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- $username = $_SESSION['username'] ?? '';
- if (!$username) {
- echo json_encode(["error" => "No username in session"]);
- exit;
- }
-
- // CSRF token check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if ($receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Get POST data.
- $data = json_decode(file_get_contents("php://input"), true);
- $oldPassword = trim($data["oldPassword"] ?? "");
- $newPassword = trim($data["newPassword"] ?? "");
- $confirmPassword = trim($data["confirmPassword"] ?? "");
-
- // Validate input.
- if (!$oldPassword || !$newPassword || !$confirmPassword) {
- echo json_encode(["error" => "All fields are required."]);
- exit;
- }
- if ($newPassword !== $confirmPassword) {
- echo json_encode(["error" => "New passwords do not match."]);
- exit;
- }
-
- // Delegate password change logic to the model.
- $result = userModel::changePassword($username, $oldPassword, $newPassword);
- echo json_encode($result);
- }
-
- /**
- * @OA\Put(
- * path="/api/updateUserPanel.php",
- * summary="Update user panel settings",
- * description="Updates user panel settings by disabling TOTP when not enabled. Accessible to authenticated users.",
- * operationId="updateUserPanel",
- * tags={"Users"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"totp_enabled"},
- * @OA\Property(property="totp_enabled", type="boolean", example=false)
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="User panel updated successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="string", example="User panel updated: TOTP disabled")
- * )
- * ),
- * @OA\Response(
- * response=401,
- * description="Unauthorized"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * )
- * )
- */
-
- public function updateUserPanel()
- {
- header('Content-Type: application/json');
-
- // Check if the user is authenticated.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(403);
- echo json_encode(["error" => "Unauthorized"]);
- exit;
- }
-
- // Verify the CSRF token.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Get the POST input.
- $data = json_decode(file_get_contents("php://input"), true);
- if (!is_array($data)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid input"]);
- exit;
- }
-
- $username = $_SESSION['username'] ?? '';
- if (!$username) {
- http_response_code(400);
- echo json_encode(["error" => "No username in session"]);
- exit;
- }
-
- // Extract totp_enabled, converting it to boolean.
- $totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false;
-
- // Delegate to the model.
- $result = userModel::updateUserPanel($username, $totp_enabled);
- echo json_encode($result);
- }
-
- /**
- * @OA\Put(
- * path="/api/totp_disable.php",
- * summary="Disable TOTP for the authenticated user",
- * description="Clears the TOTP secret from the users file for the current user.",
- * operationId="disableTOTP",
- * tags={"TOTP"},
- * @OA\Response(
- * response=200,
- * description="TOTP disabled successfully",
- * @OA\JsonContent(
- * @OA\Property(property="success", type="boolean", example=true),
- * @OA\Property(property="message", type="string", example="TOTP disabled successfully.")
- * )
- * ),
- * @OA\Response(
- * response=403,
- * description="Not authenticated or invalid CSRF token"
- * ),
- * @OA\Response(
- * response=500,
- * description="Failed to disable TOTP"
- * )
- * )
- */
-
- public function disableTOTP()
- {
- header('Content-Type: application/json');
-
- // Authentication check.
- if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
- http_response_code(403);
- echo json_encode(["error" => "Not authenticated"]);
- exit;
- }
-
- $username = $_SESSION['username'] ?? '';
- if (empty($username)) {
- http_response_code(400);
- echo json_encode(["error" => "Username not found in session"]);
- exit;
- }
-
- // CSRF token check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- // Delegate the TOTP disabling logic to the model.
- $result = userModel::disableTOTPSecret($username);
-
- if ($result) {
- echo json_encode(["success" => true, "message" => "TOTP disabled successfully."]);
- } else {
- http_response_code(500);
- echo json_encode(["error" => "Failed to disable TOTP."]);
- }
- }
-
- /**
- * @OA\Post(
- * path="/api/totp_recover.php",
- * summary="Recover TOTP",
- * description="Verifies a recovery code to disable TOTP and finalize login.",
- * operationId="recoverTOTP",
- * tags={"TOTP"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"recovery_code"},
- * @OA\Property(property="recovery_code", type="string", example="ABC123DEF456")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="Recovery successful",
- * @OA\JsonContent(
- * @OA\Property(property="status", type="string", example="ok")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Invalid input or recovery code"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token"
- * ),
- * @OA\Response(
- * response=405,
- * description="Method not allowed"
- * ),
- * @OA\Response(
- * response=429,
- * description="Too many attempts"
- * )
- * )
- */
-
- public function recoverTOTP()
- {
- header('Content-Type: application/json');
-
- // 1) Only allow POST.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- http_response_code(405);
- exit(json_encode(['status' => 'error', 'message' => 'Method not allowed']));
- }
-
- // 2) CSRF check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
- http_response_code(403);
- exit(json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']));
- }
-
- // 3) Identify the user.
- $userId = $_SESSION['username'] ?? $_SESSION['pending_login_user'] ?? null;
- if (!$userId) {
- http_response_code(401);
- exit(json_encode(['status' => 'error', 'message' => 'Unauthorized']));
- }
-
- // 4) Validate userId format.
- if (!preg_match(REGEX_USER, $userId)) {
- http_response_code(400);
- exit(json_encode(['status' => 'error', 'message' => 'Invalid user identifier']));
- }
-
- // 5) Get the recovery code from input.
- $inputData = json_decode(file_get_contents("php://input"), true);
- $recoveryCode = $inputData['recovery_code'] ?? '';
-
- // 6) Delegate to the model.
- $result = userModel::recoverTOTP($userId, $recoveryCode);
-
- if ($result['status'] === 'ok') {
- // 7) Finalize login.
- session_regenerate_id(true);
- $_SESSION['authenticated'] = true;
- $_SESSION['username'] = $userId;
- unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']);
- echo json_encode(['status' => 'ok']);
- } else {
- // Set appropriate HTTP code for errors.
- if ($result['message'] === 'Too many attempts. Try again later.') {
- http_response_code(429);
- } else {
- http_response_code(400);
- }
- echo json_encode($result);
- }
- }
-
- /**
- * @OA\Post(
- * path="/api/totp_saveCode.php",
- * summary="Generate and save a new TOTP recovery code",
- * description="Generates a new TOTP recovery code for the authenticated user, stores its hash, and returns the plain text recovery code.",
- * operationId="totpSaveCode",
- * tags={"TOTP"},
- * @OA\Response(
- * response=200,
- * description="Recovery code generated successfully",
- * @OA\JsonContent(
- * @OA\Property(property="status", type="string", example="ok"),
- * @OA\Property(property="recoveryCode", type="string", example="ABC123DEF456")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request"
- * ),
- * @OA\Response(
- * response=403,
- * description="Invalid CSRF token or unauthorized"
- * ),
- * @OA\Response(
- * response=405,
- * description="Method not allowed"
- * )
- * )
- */
-
- public function saveTOTPRecoveryCode()
- {
- header('Content-Type: application/json');
-
- // 1) Only allow POST requests.
- if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- http_response_code(405);
- error_log("totp_saveCode: invalid method {$_SERVER['REQUEST_METHOD']}");
- exit(json_encode(['status' => 'error', 'message' => 'Method not allowed']));
- }
-
- // 2) CSRF token check.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
- http_response_code(403);
- exit(json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']));
- }
-
- // 3) Ensure the user is authenticated.
- if (empty($_SESSION['username'])) {
- http_response_code(401);
- error_log("totp_saveCode: unauthorized attempt from IP {$_SERVER['REMOTE_ADDR']}");
- exit(json_encode(['status' => 'error', 'message' => 'Unauthorized']));
- }
-
- // 4) Validate the username format.
- $userId = $_SESSION['username'];
- if (!preg_match(REGEX_USER, $userId)) {
- http_response_code(400);
- error_log("totp_saveCode: invalid username format: {$userId}");
- exit(json_encode(['status' => 'error', 'message' => 'Invalid user identifier']));
- }
-
- // 5) Delegate to the model.
- $result = userModel::saveTOTPRecoveryCode($userId);
- if ($result['status'] === 'ok') {
- echo json_encode($result);
- } else {
- http_response_code(500);
- echo json_encode($result);
- }
- }
-
- /**
- * @OA\Get(
- * path="/api/totp_setup.php",
- * summary="Set up TOTP and generate a QR code",
- * description="Generates (or retrieves) the TOTP secret for the user and builds a QR code image for scanning.",
- * operationId="setupTOTP",
- * tags={"TOTP"},
- * @OA\Response(
- * response=200,
- * description="QR code image for TOTP setup",
- * @OA\MediaType(
- * mediaType="image/png"
- * )
- * ),
- * @OA\Response(
- * response=403,
- * description="Unauthorized or invalid CSRF token"
- * ),
- * @OA\Response(
- * response=500,
- * description="Server error"
- * )
- * )
- */
-
- public function setupTOTP()
- {
- // Allow access if the user is authenticated or pending TOTP.
- if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) {
- http_response_code(403);
- exit(json_encode(["error" => "Not authorized to access TOTP setup"]));
- }
-
- // Verify CSRF token from headers.
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
- if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
- }
-
- $username = $_SESSION['username'] ?? '';
- if (!$username) {
- http_response_code(400);
- exit;
- }
-
- // Set header for PNG output.
- header("Content-Type: image/png");
-
- // Delegate the TOTP setup work to the model.
- $result = userModel::setupTOTP($username);
- if (isset($result['error'])) {
- http_response_code(500);
- echo json_encode(["error" => $result['error']]);
- exit;
- }
-
- // Output the QR code image.
- echo $result['imageData'];
- }
-
- /**
- * @OA\Post(
- * path="/api/totp_verify.php",
- * summary="Verify TOTP code",
- * description="Verifies a TOTP code and completes login for pending users or validates TOTP for setup verification.",
- * operationId="verifyTOTP",
- * tags={"TOTP"},
- * @OA\RequestBody(
- * required=true,
- * @OA\JsonContent(
- * required={"totp_code"},
- * @OA\Property(property="totp_code", type="string", example="123456")
- * )
- * ),
- * @OA\Response(
- * response=200,
- * description="TOTP successfully verified",
- * @OA\JsonContent(
- * @OA\Property(property="status", type="string", example="ok"),
- * @OA\Property(property="message", type="string", example="Login successful")
- * )
- * ),
- * @OA\Response(
- * response=400,
- * description="Bad Request (e.g., invalid input)"
- * ),
- * @OA\Response(
- * response=403,
- * description="Not authenticated or invalid CSRF token"
- * ),
- * @OA\Response(
- * response=429,
- * description="Too many attempts. Try again later."
- * )
- * )
- */
-
- public function verifyTOTP()
- {
- header('Content-Type: application/json');
- header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
-
- // Rate-limit
- if (!isset($_SESSION['totp_failures'])) {
- $_SESSION['totp_failures'] = 0;
- }
- if ($_SESSION['totp_failures'] >= 5) {
- http_response_code(429);
- echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
- exit;
- }
-
- // Must be authenticated OR pending login
- if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
- http_response_code(403);
- echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
- exit;
- }
-
- // CSRF check
- $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
- $csrfHeader = $headersArr['x-csrf-token'] ?? '';
- if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
- exit;
- }
-
- // Parse & validate input
- $inputData = json_decode(file_get_contents("php://input"), true);
- $code = trim($inputData['totp_code'] ?? '');
- if (!preg_match('/^\d{6}$/', $code)) {
- http_response_code(400);
- echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
- exit;
- }
-
- // TFA helper
- $tfa = new \RobThree\Auth\TwoFactorAuth(
- new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
- 'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
- );
-
- // === Pending-login flow (we just came from auth and need to finish login) ===
- if (isset($_SESSION['pending_login_user'])) {
- $username = $_SESSION['pending_login_user'];
- $pendingSecret = $_SESSION['pending_login_secret'] ?? null;
- $rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
-
- if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
- $_SESSION['totp_failures']++;
- http_response_code(400);
- echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
- exit;
- }
-
- // Issue “remember me” token if requested
- 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' => ((int)userModel::getUserRole($username) === 1),
- 'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
- 'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
- 'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
- ];
- 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);
- }
-
- // === Finalize login into session exactly as finalizeLogin() would ===
- session_regenerate_id(true);
- $_SESSION['authenticated'] = true;
- $_SESSION['username'] = $username;
- $_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
- $perms = loadUserPermissions($username);
- $_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
- $_SESSION['readOnly'] = $perms['readOnly'] ?? false;
- $_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
-
- // Clean up pending markers
- unset(
- $_SESSION['pending_login_user'],
- $_SESSION['pending_login_secret'],
- $_SESSION['pending_login_remember_me'],
- $_SESSION['totp_failures']
- );
-
- // Send back full login payload
- echo json_encode([
- 'status' => 'ok',
- 'success' => 'Login successful',
- 'isAdmin' => $_SESSION['isAdmin'],
- 'folderOnly' => $_SESSION['folderOnly'],
- 'readOnly' => $_SESSION['readOnly'],
- 'disableUpload' => $_SESSION['disableUpload'],
- 'username' => $_SESSION['username']
- ]);
- exit;
- }
-
- // Setup/verification flow (not pending)
- $username = $_SESSION['username'] ?? '';
- if (!$username) {
- http_response_code(400);
- echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
- exit;
- }
-
- $totpSecret = userModel::getTOTPSecret($username);
- if (!$totpSecret) {
- http_response_code(500);
- echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
- exit;
- }
-
- if (!$tfa->verifyCode($totpSecret, $code)) {
- $_SESSION['totp_failures']++;
- http_response_code(400);
- echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
- exit;
- }
-
- // Successful setup/verification
- unset($_SESSION['totp_failures']);
- echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
- }
-}