From 4d329e046f6cc97d0ba31f213f8da087ea5b0827 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 4 May 2025 00:02:38 -0400 Subject: [PATCH] Remove old controllers --- src/controllers/adminController.php | 232 ---- src/controllers/authController.php | 626 ---------- src/controllers/fileController.php | 1604 -------------------------- src/controllers/folderController.php | 1107 ------------------ src/controllers/uploadController.php | 199 ---- src/controllers/userController.php | 1014 ---------------- 6 files changed, 4782 deletions(-) delete mode 100644 src/controllers/adminController.php delete mode 100644 src/controllers/authController.php delete mode 100644 src/controllers/fileController.php delete mode 100644 src/controllers/folderController.php delete mode 100644 src/controllers/uploadController.php delete mode 100644 src/controllers/userController.php 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: <?php echo htmlspecialchars($folderName, ENT_QUOTES, 'UTF-8'); ?> - - - - - -
-

Shared Folder:

-
-
- - - - -
- -

This folder is empty.

- - - - - - - - - - - - - - - - -
FilenameSize
- - - - -
- -
- - - - - - - - - -
-

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']); - } -}