Compare commits

...

13 Commits

133 changed files with 8788 additions and 5416 deletions

View File

@@ -1,5 +1,72 @@
# Changelog
## Changes 4/16 Refactor API endpoints and modularize controllers and models
- Reorganized project structure to separate API logic into dedicated controllers and models:
- Created adminController, userController, fileController, folderController, uploadController, and authController.
- Created corresponding models (AdminModel, UserModel, FileModel, FolderModel, UploadModel, AuthModel) for business logic.
- Consolidated API endpoints under the /public/api folder with subfolders for admin, auth, file, folder, and upload endpoints.
- Added inline OpenAPI annotations to document key endpoints (e.g., getConfig.php, updateConfig.php) for improved API documentation.
- Updated configuration retrieval and update logic in AdminModel and AdminController to handle OIDC and login option booleans consistently, fixing issues with basic auth settings not updating on the login page.
- Updated the client-side auth.js to correctly reference API endpoints (adjusted query selectors to reflect new document root) and load admin configuration from the updated API endpoints.
- Minor improvements to CSRF token handling, error logging, and overall code readability.
This refactor improves maintainability, testability, and documentation clarity across all API endpoints.
### Refactor fixes and adjustments
- Added fallback checks for disableFormLogin / disableBasicAuth / disableOIDCLogin when coming in either at the top level or under loginOptions.
- Updated auth.js to read and store the nested loginOptions booleans correctly in localStorage, then show/hide the BasicAuth and OIDC buttons as configured.
- Changed the logout controller to header("Location: /index.html?logout=1") so after /api/auth/logout.php it lands on the root index.html, not under /api/auth/.
- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php.
- In the sharedfolder gallery, adjusted the clientside image path to point at /uploads/... instead of /api/folder/uploads/...
- Updated both AdminModel defaults and the AuthController to use the exact full path
- Network Utilities Overhaul swapped out the old fetch wrapper for one that always reads the raw response, tries to JSON.parse it, and then either returns the parsed object on ok or throws it on error.
- Adjusted your submitLogin .catch() to grab the thrown object (or string) and pass that through to showToast, so now “Invalid credentials” actually shows up.
- Pulled the common sessionsetup and “remember me” logic into two new helpers, finalizeLogin() (for AJAX/form/basic/TOTP) and finishBrowserLogin() (for OIDC redirects). That removed tons of duplication and ensures every path calls the same permissionloading code.
- Ensured that after you POST just a totp_code, we pick up pending_login_user/pending_login_secret, verify it, then immediately call finalizeLogin().
- Expanded checkAuth.php Response now returns all three flags—folderOnly, readOnly, and disableUpload so client can handle every permission.
- In auth.jss updateAuthenticatedUI(), write all three flags into localStorage whenever you land on the app (OIDC, basic or form). That guarantees consistent behavior across page loads.
- Made sure the OIDC handler reads the live config via AdminModel::getConfig() and pushes you through the TOTP flow if needed, then back to /index.html.
- Dockerfile, custom-php.ini & start.sh moved into main repo for easier onboarding.
- filerise-docker changed to dedicated CI/CD pipeline
---
## Changes 4/15/2025
- Adjust Gallery View max columns based on screen size
- Adjust headerTitle to update globally
## Changes 4/14/2025
- Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns.
- Ensure gallery view toggle button displays after refresh page.
- Force resumable chunk size & fix chunk cleanup
### filePreview.js Enhancements
**Modal Layout Overhaul:**
- **Left Panel:** Holds zoom in/out controls at the top and the "prev" button at the bottom.
- **Center Panel:** Always centers the preview image.
- **Right Panel:** Contains rotate left/right controls at the top and the "next" button at the bottom.
**Consistent Control Presence:**
- Both left and right panels are always included. When theres only one image, placeholders are inserted in place of missing navigation buttons to ensure the image remains centered and that rotate controls are always visible.
**Improved Transform Behavior:**
- Transformation values (scale and rotation) are reset on each navigation event, ensuring predictable behavior and consistent presentation.
---
## Changes 4/13/2025 v1.1.3
- Decreased header height some more and clickable logo.
@@ -10,6 +77,41 @@
- Added translations and data attributes for almost all user-facing text
- Extend i18n support: Add new translation keys for Download and Share modals
- **Slider Integration:**
- Added a slider UI (range input, label, and value display) directly above the gallery grid.
- The slider allows users to adjust the number of columns in the gallery from 1 to 6.
- **Dynamic Grid Updates:**
- The gallery grids CSS is updated in real time via the sliders value by setting the grid-template-columns property.
- As the slider value changes, the layout instantly reflects the new column count.
- **Dynamic Image Resizing:**
- Introduced a helper function (getMaxImageHeight) that calculates the maximum image height based on the current column count.
- The max height of each image is updated immediately when the slider is adjusted to create a more dynamic display.
- **Image Caching:**
- Implemented an image caching mechanism using a global window.imageCache object.
- Images are cached on load (via an onload event) to prevent unnecessary reloading, improving performance.
- **Event Handling:**
- The sliders event listener is set up to update both the gallery grid layout and the dimensions of the thumbnails dynamically.
- Share button event listeners remain attached for proper functionality across the updated gallery view.
- **Input Validation & Security:**
- Used `filter_input()` to sanitize and validate incoming GET parameters (token, pass, page).
- Validated file system paths using `realpath()` and ensured the shared folder lies within `UPLOAD_DIR`.
- Escaped all dynamic outputs with `htmlspecialchars()` to prevent XSS.
- **Share Link Verification:**
- Loaded and validated share records from the JSON file.
- Handled expiration and password protection (with proper HTTP status codes for errors).
- **Pagination:**
- Implemented pagination by slicing the full file list into a limited number of files per page (default of 10).
- Calculated total pages and current page to create navigation links.
- **View Toggle (List vs. Gallery):**
- Added a toggle button that switches between a traditional list view and a gallery view.
- Maintained two separate view containers (`#listViewContainer` and `#galleryViewContainer`) to support this switching.
- **Gallery View with Image Caching:**
- For the gallery view, implemented a JavaScript function that creates a grid of image thumbnails.
- Each image uses a cache-busting query string on first load and caches its URL in a global `window.imageCache` for subsequent renders.
- **Persistent Pagination Controls:**
- Moved the pagination controls outside the individual view containers so that they remain visible regardless of the selected view.
---
## Changes 4/12/2025

98
Dockerfile Normal file
View File

@@ -0,0 +1,98 @@
# syntax=docker/dockerfile:1.4
#############################
# Source Stage copy your FileRise app
#############################
FROM ubuntu:24.04 AS appsource
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
# prepare the folder and remove Apaches default index
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
# **Copy the FileRise source** (where your composer.json lives)
COPY . /var/www
#############################
# Composer Stage install PHP dependencies
#############################
FROM composer:2 AS composer
WORKDIR /app
# **Copy composer files from the source** and install
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
RUN composer install --no-dev --optimize-autoloader
#############################
# Final Stage runtime image
#############################
FROM ubuntu:24.04
LABEL by=error311
# Set basic environment variables
ENV DEBIAN_FRONTEND=noninteractive \
HOME=/root \
LC_ALL=C.UTF-8 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8 \
TERM=xterm \
UPLOAD_MAX_FILESIZE=5G \
POST_MAX_SIZE=5G \
TOTAL_UPLOAD_SIZE=5G \
PERSISTENT_TOKENS_KEY=default_please_change_this_key
ARG PUID=99
ARG PGID=100
# Install Apache, PHP, and required extensions
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
apache2 php php-json php-curl php-zip php-mbstring php-gd \
ca-certificates curl git openssl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Fix www-data UID/GID
RUN set -eux; \
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g ${PGID} www-data || true; fi; \
usermod -g ${PGID} www-data
# Copy application code and vendor directory
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
COPY --from=appsource /var/www /var/www
COPY --from=composer /app/vendor /var/www/vendor
# Fix ownership & permissions
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
# Create a symlink for uploads folder in public directory.
RUN cd /var/www/public && ln -s ../uploads uploads
# Configure Apache
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/public
<Directory "/var/www/public">
AllowOverride All
Require all granted
DirectoryIndex index.php index.html
</Directory>
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
</VirtualHost>
EOF
# Enable the rewrite and headers modules
RUN a2enmod rewrite headers
# Expose ports and set up start script
EXPOSE 80 443
COPY start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh
CMD ["/usr/local/bin/start.sh"]

View File

@@ -1,86 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
$usersFile = USERS_DIR . USERS_FILE;
// Determine if we are in setup mode:
// - Query parameter setup=1 is passed
// - And users.txt is either missing or empty (zero bytes or trimmed content is empty)
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
// Allow initial admin creation without session checks.
$setupMode = true;
} else {
$setupMode = false;
// In non-setup mode, check CSRF token and require admin privileges.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
if (
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
}
// Get input data from JSON.
$data = json_decode(file_get_contents("php://input"), true);
$newUsername = trim($data["username"] ?? "");
$newPassword = trim($data["password"] ?? "");
// In setup mode, force the new user to be admin.
if ($setupMode) {
$isAdmin = "1";
} else {
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
}
// Validate input.
if (!$newUsername || !$newPassword) {
echo json_encode(["error" => "Username and password required"]);
exit;
}
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
if (!preg_match(REGEX_USER, $newUsername)) {
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
}
// Ensure users.txt exists.
if (!file_exists($usersFile)) {
file_put_contents($usersFile, '');
}
// Check if username already exists.
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($existingUsers as $line) {
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
if ($newUsername === $storedUser) {
echo json_encode(["error" => "User already exists"]);
exit;
}
}
// Hash the password.
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Prepare new user line.
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
// In setup mode, overwrite users.txt; otherwise, append to it.
if ($setupMode) {
file_put_contents($usersFile, $newUserLine);
} else {
file_put_contents($usersFile, $newUserLine, FILE_APPEND);
}
echo json_encode(["success" => "User added successfully"]);
?>

270
auth.php
View File

@@ -1,270 +0,0 @@
<?php
require_once 'vendor/autoload.php';
require_once 'config.php';
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
header('Content-Type: application/json');
// Global exception handler: logs errors and returns a generic error message.
set_exception_handler(function ($e) {
error_log("Unhandled exception: " . $e->getMessage());
http_response_code(500);
echo json_encode(["error" => "Internal Server Error"]);
exit();
});
/**
* Helper: Get the user's role from users.txt.
*/
function getUserRole($username) {
$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) >= 3 && $parts[0] === $username) {
return trim($parts[2]);
}
}
}
return null;
}
/* --- OIDC Authentication Flow --- */
// Detect either ?oidc=… or a callback that only has ?code=
$oidcAction = $_GET['oidc'] ?? null;
if (!$oidcAction && isset($_GET['code'])) {
$oidcAction = 'callback';
}
if ($oidcAction) {
$adminConfigFile = USERS_DIR . 'adminConfig.json';
if (file_exists($adminConfigFile)) {
$enc = file_get_contents($adminConfigFile);
$dec = decryptData($enc, $encryptionKey);
$cfg = $dec !== false ? json_decode($dec, true) : [];
} else {
$cfg = [];
}
$oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com';
$oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID';
$oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET';
// Use your production domain for redirect URI.
$oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/auth.php?oidc=callback';
$oidc = new Jumbojett\OpenIDConnectClient(
$oidc_provider_url,
$oidc_client_id,
$oidc_client_secret
);
$oidc->setRedirectURL($oidc_redirect_uri);
if ($oidcAction === 'callback') {
try {
$oidc->authenticate();
$username = $oidc->requestUserInfo('preferred_username');
// Check if this user has a TOTP secret.
$usersFile = USERS_DIR . USERS_FILE;
$totp_secret = null;
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 && !empty($parts[3])) {
$totp_secret = decryptData($parts[3], $encryptionKey);
break;
}
}
}
if ($totp_secret) {
// Hold pending login & prompt for TOTP.
$_SESSION['pending_login_user'] = $username;
$_SESSION['pending_login_secret'] = $totp_secret;
header("Location: index.html?totp_required=1");
exit();
}
// No TOTP → finalize login.
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = (getUserRole($username) === "1");
$_SESSION["folderOnly"] = loadUserPermissions($username);
header("Location: index.html");
exit();
} catch (Exception $e) {
error_log("OIDC authentication error: " . $e->getMessage());
http_response_code(401);
echo json_encode(["error" => "Authentication failed."]);
exit();
}
} else {
// Initiate OIDC authentication.
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();
}
}
}
/* --- Fallback: Form-based Authentication --- */
$usersFile = USERS_DIR . USERS_FILE;
$maxAttempts = 5;
$lockoutTime = 30 * 60; // 30 minutes
$attemptsFile = USERS_DIR . 'failed_logins.json';
$failedLogFile = USERS_DIR . 'failed_login.log';
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
function loadFailedAttempts($file) {
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
if (is_array($data)) {
return $data;
}
}
return [];
}
function saveFailedAttempts($file, $data) {
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
}
$ip = $_SERVER['REMOTE_ADDR'];
$currentTime = time();
$failedAttempts = loadFailedAttempts($attemptsFile);
if (isset($failedAttempts[$ip])) {
$attemptData = $failedAttempts[$ip];
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
http_response_code(429);
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
exit();
}
}
function authenticate($username, $password) {
global $usersFile, $encryptionKey;
if (!file_exists($usersFile)) {
return false;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(':', trim($line));
if (count($parts) < 3) continue;
if ($username === $parts[0] && password_verify($password, $parts[1])) {
$result = ['role' => $parts[2]];
$result['totp_secret'] = (isset($parts[3]) && !empty($parts[3]))
? decryptData($parts[3], $encryptionKey)
: null;
return $result;
}
}
return false;
}
$data = json_decode(file_get_contents("php://input"), true);
$username = trim($data["username"] ?? "");
$password = trim($data["password"] ?? "");
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
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. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit();
}
$user = authenticate($username, $password);
if ($user !== false) {
if (!empty($user['totp_secret'])) {
// If TOTP code is missing or malformed, indicate that TOTP is required.
if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) {
// ← STORE pending user & secret so recovery can see it
$_SESSION['pending_login_user'] = $username;
$_SESSION['pending_login_secret'] = $user['totp_secret'];
echo json_encode([
"totp_required" => true,
"message" => "TOTP code required"
]);
exit();
} else {
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(), // QR code provider
'FileRise', // issuer
6, // number of digits
30, // period in seconds
Algorithm::Sha1 // Correct enum case name from your enum
);
$providedCode = trim($data['totp_code']);
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
echo json_encode(["error" => "Invalid TOTP code"]);
exit();
}
}
}
if (isset($failedAttempts[$ip])) {
unset($failedAttempts[$ip]);
saveFailedAttempts($attemptsFile, $failedAttempts);
}
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($user['role'] === "1");
$_SESSION["folderOnly"] = loadUserPermissions($username);
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$expiry = time() + (30 * 24 * 60 * 60);
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
$persistentTokens[$token] = [
"username" => $username,
"expiry" => $expiry,
"isAdmin" => ($_SESSION["isAdmin"] === true)
];
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
// Define $secure based on whether HTTPS is enabled
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
}
echo json_encode([
"status" => "ok",
"success" => "Login successful",
"isAdmin" => $_SESSION["isAdmin"],
"folderOnly"=> $_SESSION["folderOnly"],
"username" => $_SESSION["username"]
]);
} else {
if (isset($failedAttempts[$ip])) {
$failedAttempts[$ip]['count']++;
$failedAttempts[$ip]['last_attempt'] = $currentTime;
} else {
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
}
saveFailedAttempts($attemptsFile, $failedAttempts);
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
http_response_code(401);
echo json_encode(["error" => "Invalid credentials"]);
}
?>

View File

@@ -1,99 +0,0 @@
<?php
// changePassword.php
require_once 'config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
$username = $_SESSION['username'] ?? '';
if (!$username) {
echo json_encode(["error" => "No username in session"]);
exit;
}
// CSRF token check.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
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;
}
// Path to users file.
$usersFile = USERS_DIR . USERS_FILE;
if (!file_exists($usersFile)) {
echo json_encode(["error" => "Users file not found"]);
exit;
}
// Read current users.
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$userFound = false;
$newLines = [];
foreach ($lines as $line) {
$parts = explode(':', trim($line));
// Expect at least 3 parts: username, hashed password, and role.
if (count($parts) < 3) {
// Skip invalid lines.
$newLines[] = $line;
continue;
}
$storedUser = $parts[0];
$storedHash = $parts[1];
$storedRole = $parts[2];
// Preserve TOTP secret if it exists.
$totpSecret = (count($parts) >= 4) ? $parts[3] : "";
if ($storedUser === $username) {
$userFound = true;
// Verify the old password.
if (!password_verify($oldPassword, $storedHash)) {
echo json_encode(["error" => "Old password is incorrect."]);
exit;
}
// Hash the new password.
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Rebuild the line with the new hash and preserve TOTP secret if present.
if ($totpSecret !== "") {
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret;
} else {
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
}
} else {
$newLines[] = $line;
}
}
if (!$userFound) {
echo json_encode(["error" => "User not found."]);
exit;
}
// Save updated users file.
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
echo json_encode(["success" => "Password updated successfully."]);
} else {
echo json_encode(["error" => "Could not update password."]);
}
?>

View File

@@ -1,70 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Check if users.txt is empty or doesn't exist.
$usersFile = USERS_DIR . USERS_FILE;
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
// In production, you might log that the system is in setup mode.
error_log("checkAuth: users file not found or empty; entering setup mode.");
echo json_encode(["setup" => true]);
exit();
}
// Check session authentication.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["authenticated" => false]);
exit();
}
/**
* Helper function to get a user's role from users.txt.
* Returns the role as a string (e.g. "1") or null if not found.
*/
function getUserRole($username) {
global $usersFile;
if (file_exists($usersFile)) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(":", trim($line));
if (count($parts) >= 3 && $parts[0] === $username) {
return trim($parts[2]);
}
}
}
return null;
}
// Determine if TOTP is enabled by checking users.txt.
$totp_enabled = false;
$username = $_SESSION['username'] ?? '';
if ($username) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(":", trim($line));
// Assuming first field is username and fourth (if exists) is the TOTP secret.
if ($parts[0] === $username) {
if (isset($parts[3]) && trim($parts[3]) !== "") {
$totp_enabled = true;
}
break;
}
}
}
// Use getUserRole() to determine admin status.
// We cast the role to an integer so that "1" (string) is treated as true.
$userRole = getUserRole($username);
$isAdmin = ((int)$userRole === 1);
// Build and return the JSON response.
$response = [
"authenticated" => true,
"isAdmin" => $isAdmin,
"totp_enabled" => $totp_enabled,
"username" => $username,
"folderOnly" => isset($_SESSION["folderOnly"]) ? $_SESSION["folderOnly"] : false
];
echo json_encode($response);
?>

View File

@@ -17,6 +17,7 @@ header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
header("X-XSS-Protection: 1; mode=block");
// Define constants.
define('PROJECT_ROOT', dirname(__DIR__));
define('UPLOAD_DIR', '/var/www/uploads/');
define('USERS_DIR', '/var/www/users/');
define('USERS_FILE', 'users.txt');
@@ -116,7 +117,11 @@ $cookieParams = [
'httponly' => true,
'samesite' => 'Lax'
];
// At the very beginning of config.php
/*ini_set('session.save_path', __DIR__ . '/../sessions');
if (!is_dir(__DIR__ . '/../sessions')) {
mkdir(__DIR__ . '/../sessions', 0777, true);
}*/
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params($cookieParams);
ini_set('session.gc_maxlifetime', 7200);
@@ -159,9 +164,9 @@ define('BASE_URL', 'http://yourwebsite/uploads/');
if (strpos(BASE_URL, 'yourwebsite') !== false) {
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
: "http://localhost/share.php";
? "http://" . $_SERVER['HTTP_HOST'] . "/api/file/share.php"
: "http://localhost/api/file/share.php";
} else {
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
$defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php";
}
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);

View File

@@ -1,153 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to copy files."]);
exit();
}
}
$data = json_decode(file_get_contents("php://input"), true);
if (
!$data ||
!isset($data['source']) ||
!isset($data['destination']) ||
!isset($data['files'])
) {
echo json_encode(["error" => "Invalid request"]);
exit;
}
$sourceFolder = trim($data['source']);
$destinationFolder = trim($data['destination']);
$files = $data['files'];
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
$folderPattern = REGEX_FOLDER_NAME;
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
}
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
echo json_encode(["error" => "Invalid destination folder name."]);
exit;
}
// Trim any leading/trailing slashes and spaces.
$sourceFolder = trim($sourceFolder, "/\\ ");
$destinationFolder = trim($destinationFolder, "/\\ ");
// Build the source and destination directories.
$baseDir = rtrim(UPLOAD_DIR, '/\\');
$sourceDir = ($sourceFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
// Helper: Generate the metadata file path for a given folder.
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
// Helper: Generate a unique file name if a file with the same name exists.
function getUniqueFileName($destDir, $fileName) {
$fullPath = $destDir . $fileName;
clearstatcache(true, $fullPath);
if (!file_exists($fullPath)) {
return $fileName;
}
$basename = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$counter = 1;
do {
$newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : "");
$newFullPath = $destDir . $newName;
clearstatcache(true, $newFullPath);
$counter++;
} while (file_exists($destDir . $newName));
return $newName;
}
// Load source and destination metadata.
$srcMetaFile = getMetadataFilePath($sourceFolder);
$destMetaFile = getMetadataFilePath($destinationFolder);
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($files as $fileName) {
// Save the original name for metadata lookup.
$originalName = basename(trim($fileName));
$basename = $originalName;
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has an invalid name.";
continue;
}
$srcPath = $sourceDir . $originalName;
$destPath = $destDir . $basename;
clearstatcache();
if (!file_exists($srcPath)) {
$errors[] = "$originalName does not exist in source.";
continue;
}
if (file_exists($destPath)) {
$uniqueName = getUniqueFileName($destDir, $basename);
$basename = $uniqueName; // update the file name for metadata and destination path
$destPath = $destDir . $uniqueName;
}
if (!copy($srcPath, $destPath)) {
$errors[] = "Failed to copy $basename";
continue;
}
// Update destination metadata: if there's metadata for the original file in source, add it under the new name.
if (isset($srcMetadata[$originalName])) {
$destMetadata[$basename] = $srcMetadata[$originalName];
}
}
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update destination metadata.";
}
if (empty($errors)) {
echo json_encode(["success" => "Files copied successfully"]);
} else {
echo json_encode(["error" => implode("; ", $errors)]);
}
?>

View File

@@ -1,96 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// Ensure the request is a POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Invalid request method.']);
exit;
}
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
http_response_code(403);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
exit();
}
}
// Get the JSON input and decode it
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['folderName'])) {
echo json_encode(['success' => false, 'error' => 'Folder name not provided.']);
exit;
}
$folderName = trim($input['folderName']);
$parent = isset($input['parent']) ? trim($input['parent']) : "";
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit;
}
// Optionally, sanitize the parent folder if needed.
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
exit;
}
// Build the full folder path.
$baseDir = rtrim(UPLOAD_DIR, '/\\');
if ($parent && strtolower($parent) !== "root") {
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
$relativePath = $parent . "/" . $folderName;
} else {
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
$relativePath = $folderName;
}
// Check if the folder already exists.
if (file_exists($fullPath)) {
echo json_encode(['success' => false, 'error' => 'Folder already exists.']);
exit;
}
// Attempt to create the folder.
if (mkdir($fullPath, 0755, true)) {
// --- Create an empty metadata file for the new folder ---
// Helper: Generate the metadata file path for a given folder.
// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json".
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$metadataFile = getMetadataFilePath($relativePath);
// Create an empty associative array (i.e. empty metadata) and write to the metadata file.
file_put_contents($metadataFile, json_encode([], JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Failed to create folder.']);
}
?>

View File

@@ -1,94 +0,0 @@
<?php
// createFolderShareLink.php
require_once 'config.php';
// Get POST input.
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
echo json_encode(["error" => "Invalid input."]);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to create shared folders."]);
exit();
}
}
$folder = isset($input['folder']) ? trim($input['folder']) : "";
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
$password = isset($input['password']) ? $input['password'] : "";
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
// Validate folder name using regex.
// Allow letters, numbers, underscores, hyphens, spaces and slashes.
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
// Generate a secure token.
try {
$token = bin2hex(random_bytes(16)); // 32 hex characters.
} catch (Exception $e) {
echo json_encode(["error" => "Could not generate token."]);
exit;
}
// Calculate expiration time (Unix timestamp).
$expires = time() + ($expirationMinutes * 60);
// Hash password if provided.
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
// Define the file to store share folder links.
$shareFile = META_DIR . "share_folder_links.json";
$shareLinks = [];
if (file_exists($shareFile)) {
$data = file_get_contents($shareFile);
$shareLinks = json_decode($data, true);
if (!is_array($shareLinks)) {
$shareLinks = [];
}
}
// Clean up expired share links.
$currentTime = time();
foreach ($shareLinks as $key => $link) {
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
unset($shareLinks[$key]);
}
}
// Add the new share record.
$shareLinks[$token] = [
"folder" => $folder,
"expires" => $expires,
"password" => $hashedPassword,
"allowUpload" => $allowUpload
];
// Save the share links.
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
// Determine base URL.
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
$baseUrl = rtrim(BASE_URL, '/');
} else {
// Prefer HTTP_HOST over SERVER_ADDR.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
// Use HTTP_HOST if set; fallback to gethostbyname if needed.
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
$baseUrl = $protocol . "://" . $host;
}
// The share URL points to shareFolder.php.
$link = $baseUrl . "/shareFolder.php?token=" . urlencode($token);
echo json_encode(["token" => $token, "expires" => $expires, "link" => $link]);
} else {
echo json_encode(["error" => "Could not save share link."]);
}
?>

View File

@@ -1,75 +0,0 @@
<?php
// createShareLink.php
require_once 'config.php';
// Get POST input.
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
echo json_encode(["error" => "Invalid input."]);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to create share files."]);
exit();
}
}
$folder = isset($input['folder']) ? trim($input['folder']) : "";
$file = isset($input['file']) ? basename($input['file']) : "";
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
$password = isset($input['password']) ? $input['password'] : "";
// Validate folder using regex.
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
// Generate a secure token.
$token = bin2hex(random_bytes(16)); // 32 hex characters.
// Calculate expiration (Unix timestamp).
$expires = time() + ($expirationMinutes * 60);
// Hash password if provided.
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
// File to store share links.
$shareFile = META_DIR . "share_links.json";
$shareLinks = [];
if (file_exists($shareFile)) {
$data = file_get_contents($shareFile);
$shareLinks = json_decode($data, true);
if (!is_array($shareLinks)) {
$shareLinks = [];
}
}
// Clean up expired share links.
$currentTime = time();
foreach ($shareLinks as $key => $link) {
if ($link["expires"] < $currentTime) {
unset($shareLinks[$key]);
}
}
// Add record.
$shareLinks[$token] = [
"folder" => $folder,
"file" => $file,
"expires" => $expires,
"password" => $hashedPassword
];
// Save the share links.
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
echo json_encode(["token" => $token, "expires" => $expires]);
} else {
echo json_encode(["error" => "Could not save share link."]);
}
?>

52
custom-php.ini Normal file
View File

@@ -0,0 +1,52 @@
; custom-php.ini
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; OPcache Settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
opcache.enable=1
opcache.enable_cli=0
; Allocate 128MB of memory for opcode caching
opcache.memory_consumption=128
; Increase the maximum number of accelerated files (adjust if you have a large codebase)
opcache.max_accelerated_files=4000
; Refresh file timestamp every 60 seconds to avoid too many disk reads
opcache.revalidate_freq=60
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Memory and Execution Time Limits
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Increase memory limit to 512M for large file processing or image processing operations
memory_limit=512M
; Set execution time limits to accommodate long-running uploads/processes
max_execution_time=300
max_input_time=300
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Realpath Cache Settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
realpath_cache_size=4096k
realpath_cache_ttl=600
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; File Upload Settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Allow a maximum of 20 files per request
max_file_uploads=20
; Ensure the temporary directory is set (should exist and be writable)
upload_tmp_dir=/tmp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Session Configuration (if applicable)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
session.gc_maxlifetime=1440
session.gc_probability=1
session.gc_divisor=100
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Error Handling / Logging
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Do not display errors publicly in production
display_errors=Off
; Log errors to a dedicated file
log_errors=On
error_log=/var/log/php8.3-error.log

View File

@@ -1,161 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// Define $username first.
$username = $_SESSION['username'] ?? '';
// Now load the user's permissions.
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only.
if ($username) {
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to delete files."]);
exit();
}
}
// --- Setup Trash Folder & Metadata ---
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
if (!file_exists($trashDir)) {
mkdir($trashDir, 0755, true);
}
$trashMetadataFile = $trashDir . "trash.json";
$trashData = [];
if (file_exists($trashMetadataFile)) {
$json = file_get_contents($trashMetadataFile);
$trashData = json_decode($json, true);
if (!is_array($trashData)) {
$trashData = [];
}
}
// Helper: Generate the metadata file path for a given folder.
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
// Read request body
$data = json_decode(file_get_contents("php://input"), true);
// Validate request
if (!isset($data['files']) || !is_array($data['files'])) {
echo json_encode(["error" => "No file names provided"]);
exit;
}
// Determine folder default to 'root'
$folder = isset($data['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;
}
$folder = trim($folder, "/\\ ");
// Build the upload directory.
if ($folder !== 'root') {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
}
// Load folder metadata (if exists) to retrieve uploader and upload date.
$metadataFile = getMetadataFilePath($folder);
$folderMetadata = [];
if (file_exists($metadataFile)) {
$folderMetadata = json_decode(file_get_contents($metadataFile), true);
if (!is_array($folderMetadata)) {
$folderMetadata = [];
}
}
$movedFiles = [];
$errors = [];
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($data['files'] as $fileName) {
$basename = basename(trim($fileName));
// Validate the file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has an invalid name.";
continue;
}
$filePath = $uploadDir . $basename;
if (file_exists($filePath)) {
// Append a timestamp to the file name in trash to avoid collisions.
$timestamp = time();
$trashFileName = $basename . "_" . $timestamp;
if (rename($filePath, $trashDir . $trashFileName)) {
$movedFiles[] = $basename;
// Record trash metadata for possible restoration.
$trashData[] = [
'type' => 'file',
'originalFolder' => $uploadDir, // You could also store a relative path here.
'originalName' => $basename,
'trashName' => $trashFileName,
'trashedAt' => $timestamp,
// Enrich trash record with uploader and upload date from folder metadata (if available)
'uploaded' => isset($folderMetadata[$basename]['uploaded']) ? $folderMetadata[$basename]['uploaded'] : "Unknown",
'uploader' => isset($folderMetadata[$basename]['uploader']) ? $folderMetadata[$basename]['uploader'] : "Unknown",
// NEW: Record the username of the user who deleted the file.
'deletedBy' => isset($_SESSION['username']) ? $_SESSION['username'] : "Unknown"
];
} else {
$errors[] = "Failed to move $basename to Trash.";
}
} else {
// Consider file already deleted.
$movedFiles[] = $basename;
}
}
// Write back the updated trash metadata.
file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT));
// Update folder-specific metadata file by removing deleted files.
if (file_exists($metadataFile)) {
$metadata = json_decode(file_get_contents($metadataFile), true);
if (is_array($metadata)) {
foreach ($movedFiles as $delFile) {
if (isset($metadata[$delFile])) {
unset($metadata[$delFile]);
}
}
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
}
}
if (empty($errors)) {
echo json_encode(["success" => "Files moved to Trash: " . implode(", ", $movedFiles)]);
} else {
echo json_encode(["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)]);
}
?>

View File

@@ -1,99 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// Ensure the request is a POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Invalid request method.']);
exit;
}
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
http_response_code(403);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to delete folders."]);
exit();
}
}
// Get the JSON input and decode it
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['folder'])) {
echo json_encode(['success' => false, 'error' => 'Folder name not provided.']);
exit;
}
$folderName = trim($input['folder']);
// Prevent deletion of root.
if ($folderName === 'root') {
echo json_encode(['success' => false, 'error' => 'Cannot delete root folder.']);
exit;
}
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit;
}
// Build the folder path (supports subfolder paths like "FolderTest/FolderTestSub")
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName;
// Check if the folder exists and is a directory
if (!file_exists($folderPath) || !is_dir($folderPath)) {
echo json_encode(['success' => false, 'error' => 'Folder does not exist.']);
exit;
}
// Prevent deletion if the folder is not empty
if (count(scandir($folderPath)) > 2) {
echo json_encode(['success' => false, 'error' => 'Folder is not empty.']);
exit;
}
/**
* Helper: Generate the metadata file path for a given folder.
* For "root", returns "root_metadata.json". Otherwise, it replaces
* slashes, backslashes, and spaces with dashes and appends "_metadata.json".
*
* @param string $folder The folder's relative path.
* @return string The full path to the folder's metadata file.
*/
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
// Attempt to delete the folder.
if (rmdir($folderPath)) {
// Remove corresponding metadata file if it exists.
$metadataFile = getMetadataFilePath($folderName);
if (file_exists($metadataFile)) {
unlink($metadataFile);
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']);
}
?>

View File

@@ -1,104 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// --- Setup Trash Folder & Metadata ---
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
if (!file_exists($trashDir)) {
mkdir($trashDir, 0755, true);
}
$trashMetadataFile = $trashDir . "trash.json";
// Load trash metadata into an associative array keyed by trashName.
$trashData = [];
if (file_exists($trashMetadataFile)) {
$json = file_get_contents($trashMetadataFile);
$tempData = json_decode($json, true);
if (is_array($tempData)) {
foreach ($tempData as $item) {
if (isset($item['trashName'])) {
$trashData[$item['trashName']] = $item;
}
}
}
}
// Read request body.
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["error" => "Invalid input"]);
exit;
}
// Determine deletion mode: if "deleteAll" is true, delete all trash items; otherwise, use provided "files" array.
$filesToDelete = [];
if (isset($data['deleteAll']) && $data['deleteAll'] === true) {
$filesToDelete = array_keys($trashData);
} elseif (isset($data['files']) && is_array($data['files'])) {
$filesToDelete = $data['files'];
} else {
echo json_encode(["error" => "No trash file identifiers provided"]);
exit;
}
$deletedFiles = [];
$errors = [];
// Define a safe file name pattern.
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($filesToDelete as $trashName) {
$trashName = trim($trashName);
if (!preg_match($safeFileNamePattern, $trashName)) {
$errors[] = "$trashName has an invalid format.";
continue;
}
if (!isset($trashData[$trashName])) {
$errors[] = "Trash item $trashName not found.";
continue;
}
$filePath = $trashDir . $trashName;
if (file_exists($filePath)) {
if (unlink($filePath)) {
$deletedFiles[] = $trashName;
unset($trashData[$trashName]);
} else {
$errors[] = "Failed to delete $trashName.";
}
} else {
// If the file doesn't exist, remove its metadata entry.
unset($trashData[$trashName]);
$deletedFiles[] = $trashName;
}
}
// Write the updated trash metadata back (as an indexed array).
file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT));
if (empty($errors)) {
echo json_encode(["success" => "Trash items deleted: " . implode(", ", $deletedFiles)]);
} else {
echo json_encode(["error" => implode("; ", $errors) . ". Trash items deleted: " . implode(", ", $deletedFiles)]);
}
exit;
?>

View File

@@ -1,85 +0,0 @@
<?php
require_once 'config.php';
// 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 file parameters from the GET request.
$file = isset($_GET['file']) ? basename($_GET['file']) : '';
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses)
if (!preg_match(REGEX_FILE_NAME, $file)) {
http_response_code(400);
echo json_encode(["error" => "Invalid file name."]);
exit;
}
// Get the realpath of the upload directory.
$uploadDirReal = realpath(UPLOAD_DIR);
if ($uploadDirReal === false) {
http_response_code(500);
echo json_encode(["error" => "Server misconfiguration."]);
exit;
}
// Determine the directory.
if ($folder === 'root') {
$directory = $uploadDirReal;
} else {
// Prevent path traversal in folder parameter.
if (strpos($folder, '..') !== false) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
$directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$directory = realpath($directoryPath);
// Ensure that the resolved directory exists and is within the allowed UPLOAD_DIR.
if ($directory === false || strpos($directory, $uploadDirReal) !== 0) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder path."]);
exit;
}
}
// Build the file path.
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
// Validate that the real file path exists and is within the allowed directory.
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
http_response_code(403);
echo json_encode(["error" => "Access forbidden."]);
exit;
}
if (!file_exists($realFilePath)) {
http_response_code(404);
echo json_encode(["error" => "File not found."]);
exit;
}
// Serve the file.
$mimeType = mime_content_type($realFilePath);
header("Content-Type: " . $mimeType);
// For images, serve inline; for other types, force download.
$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;
?>

View File

@@ -1,85 +0,0 @@
<?php
// downloadSharedFile.php
require_once 'config.php';
// Retrieve and sanitize token and file name from GET.
$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);
echo "Missing token or file parameter.";
exit;
}
// Load the share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$shareLinks = json_decode(file_get_contents($shareFile), true);
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$record = $shareLinks[$token];
// Check if the link has expired.
if (time() > $record['expires']) {
http_response_code(403);
echo "This share link has expired.";
exit;
}
// Get the shared folder from the record.
$folder = trim($record['folder'], "/\\ ");
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realFolderPath = realpath($folderPath);
$uploadDirReal = realpath(UPLOAD_DIR);
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
http_response_code(404);
echo "Shared folder not found.";
exit;
}
// Sanitize the filename to prevent directory traversal.
if (strpos($file, "/") !== false || strpos($file, "\\") !== false) {
http_response_code(400);
echo "Invalid file name.";
exit;
}
$file = basename($file);
// Build the full file path and verify it is inside the shared folder.
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
http_response_code(404);
echo "File not found.";
exit;
}
// Determine MIME type.
$mimeType = mime_content_type($realFilePath);
header("Content-Type: " . $mimeType);
// Set Content-Disposition header.
// Inline if the file is an image; attachment for others.
$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) . '"');
}
// Read and output the file.
readfile($realFilePath);
exit;
?>

View File

@@ -1,133 +0,0 @@
<?php
require_once 'config.php';
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// 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;
}
// Read and decode the JSON input.
$rawData = file_get_contents("php://input");
$data = json_decode($rawData, 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 name to allow subfolders.
// "root" is allowed; otherwise, split by "/" 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;
}
}
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
} else {
$relativePath = "";
}
// Use the absolute UPLOAD_DIR from config.php.
$baseDir = realpath(UPLOAD_DIR);
if ($baseDir === false) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(["error" => "Uploads directory not configured correctly."]);
exit;
}
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
$folderPathReal = realpath($folderPath);
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(["error" => "Folder not found."]);
exit;
}
if (empty($files)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(["error" => "No files specified."]);
exit;
}
foreach ($files as $fileName) {
if (!preg_match(REGEX_FILE_NAME, $fileName)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(["error" => "Invalid file name: " . $fileName]);
exit;
}
}
// Build an array of files to include in the ZIP.
$filesToZip = [];
foreach ($files as $fileName) {
$filePath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName;
if (file_exists($filePath)) {
$filesToZip[] = $filePath;
}
}
if (empty($filesToZip)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(["error" => "No valid files found to zip."]);
exit;
}
// Create a temporary file for the ZIP archive.
$tempZip = tempnam(sys_get_temp_dir(), 'zip');
unlink($tempZip); // Remove the temporary file so ZipArchive can create a new one.
$tempZip .= '.zip';
$zip = new ZipArchive();
if ($zip->open($tempZip, ZipArchive::CREATE) !== TRUE) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(["error" => "Could not create zip archive."]);
exit;
}
// Add each file to the archive using its base name.
foreach ($filesToZip as $filePath) {
$zip->addFile($filePath, basename($filePath));
}
$zip->close();
// Send headers to force download and disable caching.
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="files.zip"');
header('Content-Length: ' . filesize($tempZip));
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
// Output the file and delete it afterward.
readfile($tempZip);
unlink($tempZip);
exit;
?>

View File

@@ -1,165 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($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;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to extract zip files"]);
exit();
}
}
// Read and decode the JSON input.
$rawData = file_get_contents("php://input");
$data = json_decode($rawData, 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'];
if (empty($files)) {
http_response_code(400);
echo json_encode(["error" => "No files specified."]);
exit;
}
// Validate folder name (allow "root" or valid subfolder names).
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);
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
}
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
} else {
$relativePath = "";
}
$baseDir = realpath(UPLOAD_DIR);
if ($baseDir === false) {
http_response_code(500);
echo json_encode(["error" => "Uploads directory not configured correctly."]);
exit;
}
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
$folderPathReal = realpath($folderPath);
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
http_response_code(404);
echo json_encode(["error" => "Folder not found."]);
exit;
}
// ---------- Metadata Setup ----------
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$srcMetaFile = getMetadataFilePath($folder);
$destMetaFile = getMetadataFilePath($folder);
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
$allSuccess = true;
$extractedFiles = array(); // Array to collect names of extracted files
$safeFileNamePattern = REGEX_FILE_NAME;
// ---------- Process Each File ----------
foreach ($files as $zipFileName) {
$originalName = basename(trim($zipFileName));
// Process only .zip files.
if (strtolower(substr($originalName, -4)) !== '.zip') {
continue;
}
if (!preg_match($safeFileNamePattern, $originalName)) {
$errors[] = "$originalName has an invalid name.";
$allSuccess = false;
continue;
}
$zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName;
if (!file_exists($zipFilePath)) {
$errors[] = "$originalName does not exist in folder.";
$allSuccess = false;
continue;
}
$zip = new ZipArchive();
if ($zip->open($zipFilePath) !== TRUE) {
$errors[] = "Could not open $originalName as a zip file.";
$allSuccess = false;
continue;
}
// Attempt extraction.
if (!$zip->extractTo($folderPathReal)) {
$errors[] = "Failed to extract $originalName.";
$allSuccess = false;
} else {
// Collect extracted file names from this zip.
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$extractedFileName = basename($entryName);
if ($extractedFileName) {
$extractedFiles[] = $extractedFileName;
}
}
// Update metadata for each extracted file if the zip file has metadata.
if (isset($srcMetadata[$originalName])) {
$zipMeta = $srcMetadata[$originalName];
// Iterate through all entries in the zip.
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$extractedFileName = basename($entryName);
if ($extractedFileName) {
$destMetadata[$extractedFileName] = $zipMeta;
}
}
}
}
$zip->close();
}
// Write updated metadata back to the destination metadata file.
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update metadata.";
$allSuccess = false;
}
if ($allSuccess) {
echo json_encode(["success" => true, "extractedFiles" => $extractedFiles]);
} else {
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
}
exit;
?>

View File

@@ -1,46 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
$configFile = USERS_DIR . 'adminConfig.json';
if (file_exists($configFile)) {
$encryptedContent = file_get_contents($configFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
if ($decryptedContent === false) {
http_response_code(500);
echo json_encode(['error' => 'Failed to decrypt configuration.']);
exit;
}
// Decode the configuration and ensure required fields are set
$config = json_decode($decryptedContent, true);
// Ensure globalOtpauthUrl is set
if (!isset($config['globalOtpauthUrl'])) {
$config['globalOtpauthUrl'] = "";
}
// NEW: Ensure header_title is set.
if (!isset($config['header_title']) || empty($config['header_title'])) {
$config['header_title'] = "FileRise"; // default value
}
echo json_encode($config);
} else {
// If no config file exists, provide defaults
echo json_encode([
'header_title' => "FileRise",
'oidc' => [
'providerUrl' => 'https://your-oidc-provider.com',
'clientId' => 'YOUR_CLIENT_ID',
'clientSecret' => 'YOUR_CLIENT_SECRET',
'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback'
],
'loginOptions' => [
'disableFormLogin' => false,
'disableBasicAuth' => false,
'disableOIDCLogin' => false
],
'globalOtpauthUrl' => ""
]);
}
?>

View File

@@ -1,106 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Allow only safe characters in the folder parameter (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;
}
// Determine the directory based on the folder parameter.
if ($folder !== 'root') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
} else {
$directory = UPLOAD_DIR;
}
/**
* Helper: Generate the metadata file path for a given folder.
*/
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$metadataFile = getMetadataFilePath($folder);
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
if (!is_dir($directory)) {
echo json_encode(["error" => "Directory not found."]);
exit;
}
$files = array_values(array_diff(scandir($directory), array('.', '..')));
$fileList = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($files as $file) {
// Skip hidden files (those that begin with a dot)
if (substr($file, 0, 1) === '.') {
continue;
}
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
// Only include files (skip directories)
if (!is_file($filePath)) continue;
// Optionally, skip files with unsafe names.
if (!preg_match($safeFileNamePattern, $file)) {
continue;
}
// Since metadata is stored per folder, the key is simply the file name.
$metaKey = $file;
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
$fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
$fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown";
$fileSizeBytes = filesize($filePath);
if ($fileSizeBytes >= 1073741824) {
$fileSizeFormatted = sprintf("%.1f GB", $fileSizeBytes / 1073741824);
} elseif ($fileSizeBytes >= 1048576) {
$fileSizeFormatted = sprintf("%.1f MB", $fileSizeBytes / 1048576);
} elseif ($fileSizeBytes >= 1024) {
$fileSizeFormatted = sprintf("%.1f KB", $fileSizeBytes / 1024);
} else {
$fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes));
}
// Build the basic file entry.
$fileEntry = [
'name' => $file,
'modified' => $fileDateModified,
'uploaded' => $fileUploadedDate,
'size' => $fileSizeFormatted,
'uploader' => $fileUploader,
'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : []
];
// Add file content for text-based files.
if (preg_match('/\.(txt|html|htm|md|js|css|json|xml|php|py|ini|conf|log)$/i', $file)) {
$content = file_get_contents($filePath);
$fileEntry['content'] = $content;
}
$fileList[] = $fileEntry;
}
// Load global tags from createdTags.json.
$globalTagsFile = META_DIR . "createdTags.json";
$globalTags = file_exists($globalTagsFile) ? json_decode(file_get_contents($globalTagsFile), true) : [];
echo json_encode(["files" => $fileList, "globalTags" => $globalTags]);
?>

View File

@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
// getFileTag.php
require_once 'config.php';
// Set security and content headers
header('Content-Type: application/json; charset=utf-8');
$metadataPath = META_DIR . 'createdTags.json';
// Check if the metadata file exists and is readable
if (!file_exists($metadataPath) || !is_readable($metadataPath)) {
error_log('Metadata file does not exist or is not readable: ' . $metadataPath);
http_response_code(200); // Return empty array with HTTP 200 so the client can handle it gracefully
echo json_encode([]);
exit;
}
$data = file_get_contents($metadataPath);
if ($data === false) {
error_log('Failed to read metadata file: ' . $metadataPath);
http_response_code(500);
echo json_encode(["error" => "Unable to read metadata file."]);
exit;
}
// Decode the JSON data to check for validity
$jsonData = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Invalid JSON in metadata file: ' . $metadataPath . ' Error: ' . json_last_error_msg());
http_response_code(500);
echo json_encode(["error" => "Metadata file contains invalid JSON."]);
exit;
}
// Output the re-encoded JSON to ensure well-formed output
echo json_encode($jsonData);
exit;

View File

@@ -1,97 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
/**
* Recursively scan a directory for subfolders.
*
* @param string $dir The full path to the directory.
* @param string $relative The relative path from the base upload directory.
* @return array An array of folder paths (relative to the base).
*/
function getSubfolders($dir, $relative = '') {
$folders = [];
$items = scandir($dir);
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
$safeFolderNamePattern = REGEX_FOLDER_NAME;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
if (!preg_match($safeFolderNamePattern, $item)) {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
// Build the relative path.
$folderPath = ($relative ? $relative . '/' : '') . $item;
$folders[] = $folderPath;
// Recursively get subfolders.
$subFolders = getSubfolders($path, $folderPath);
$folders = array_merge($folders, $subFolders);
}
}
return $folders;
}
/**
* Helper: Generate the metadata file path for a given folder.
* For "root", it returns "root_metadata.json"; otherwise, it replaces
* slashes, backslashes, and spaces with dashes and appends "_metadata.json".
*
* @param string $folder The folder's relative path.
* @return string The full path to the folder's metadata file.
*/
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$baseDir = rtrim(UPLOAD_DIR, '/\\');
// Build an array to hold folder information.
$folderInfoList = [];
// Include "root" as a folder.
$rootMetaFile = getMetadataFilePath('root');
$rootFileCount = 0;
if (file_exists($rootMetaFile)) {
$rootMetadata = json_decode(file_get_contents($rootMetaFile), true);
$rootFileCount = is_array($rootMetadata) ? count($rootMetadata) : 0;
}
$folderInfoList[] = [
"folder" => "root",
"fileCount" => $rootFileCount,
"metadataFile" => basename($rootMetaFile)
];
// Scan for subfolders.
$subfolders = [];
if (is_dir($baseDir)) {
$subfolders = getSubfolders($baseDir);
}
// For each subfolder, load its metadata and record file count.
foreach ($subfolders as $folder) {
$metaFile = getMetadataFilePath($folder);
$fileCount = 0;
if (file_exists($metaFile)) {
$metadata = json_decode(file_get_contents($metaFile), true);
$fileCount = is_array($metadata) ? count($metadata) : 0;
}
$folderInfoList[] = [
"folder" => $folder,
"fileCount" => $fileCount,
"metadataFile" => basename($metaFile)
];
}
echo json_encode($folderInfoList);
?>

View File

@@ -1,68 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Ensure user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// Define the trash directory and trash metadata file.
$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
$trashMetadataFile = $trashDir . "trash.json";
// Helper: Generate the metadata file path for a given folder.
// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json".
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
// Read the trash metadata.
$trashItems = [];
if (file_exists($trashMetadataFile)) {
$json = file_get_contents($trashMetadataFile);
$trashItems = json_decode($json, true);
if (!is_array($trashItems)) {
$trashItems = [];
}
}
// Enrich each trash record.
foreach ($trashItems as &$item) {
// Ensure deletedBy is set and not empty.
if (empty($item['deletedBy'])) {
$item['deletedBy'] = "Unknown";
}
// Enrich with uploader and uploaded date if not already present.
if (empty($item['uploaded']) || empty($item['uploader'])) {
if (isset($item['originalFolder']) && isset($item['originalName'])) {
$metadataFile = getMetadataFilePath($item['originalFolder']);
if (file_exists($metadataFile)) {
$metadata = json_decode(file_get_contents($metadataFile), true);
if (is_array($metadata) && isset($metadata[$item['originalName']])) {
$item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown";
$item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown";
} else {
$item['uploaded'] = "Unknown";
$item['uploader'] = "Unknown";
}
} else {
$item['uploaded'] = "Unknown";
$item['uploader'] = "Unknown";
}
} else {
$item['uploaded'] = "Unknown";
$item['uploader'] = "Unknown";
}
}
}
unset($item);
echo json_encode($trashItems);
exit;
?>

View File

@@ -1,47 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Check if the user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
$permissionsFile = USERS_DIR . "userPermissions.json";
$permissionsArray = [];
// Load permissions file if it exists.
if (file_exists($permissionsFile)) {
$content = file_get_contents($permissionsFile);
// Attempt to decrypt the content.
$decryptedContent = decryptData($content, $encryptionKey);
if ($decryptedContent === false) {
// If decryption fails, assume the file is plain JSON.
$permissionsArray = json_decode($content, true);
} else {
$permissionsArray = json_decode($decryptedContent, true);
}
if (!is_array($permissionsArray)) {
$permissionsArray = [];
}
}
// If the user is an admin, return all permissions.
if (isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true) {
echo json_encode($permissionsArray);
exit;
}
// Otherwise, return only the current user's permissions.
$username = $_SESSION['username'] ?? '';
foreach ($permissionsArray as $storedUsername => $data) {
if (strcasecmp($storedUsername, $username) === 0) {
echo json_encode($data);
exit;
}
}
// If no permissions are found for the current user, return an empty object.
echo json_encode(new stdClass());
?>

View File

@@ -1,31 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
$usersFile = USERS_DIR . USERS_FILE;
$users = [];
if (file_exists($usersFile)) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(':', trim($line));
if (count($parts) >= 3) {
// Validate username format:
if (preg_match(REGEX_USER, $parts[0])) {
$users[] = [
"username" => $parts[0],
"role" => trim($parts[2])
];
}
}
}
}
echo json_encode($users);
?>

View File

@@ -1,31 +0,0 @@
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
const options = {
method,
credentials: 'include',
headers: {}
};
// Merge custom headers
Object.assign(options.headers, customHeaders);
// If data is provided and is not FormData, assume JSON.
if (data && !(data instanceof FormData)) {
if (!options.headers["Content-Type"]) {
options.headers["Content-Type"] = "application/json";
}
options.body = JSON.stringify(data);
} else if (data instanceof FormData) {
options.body = data;
}
return fetch(url, options)
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP error ${response.status}: ${text}`);
});
}
const clonedResponse = response.clone();
return response.json().catch(() => clonedResponse.text());
});
}

View File

@@ -1,120 +0,0 @@
<?php
require_once 'config.php';
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
// Helper: retrieve a user's TOTP secret from users.txt
function getUserTOTPSecret($username) {
global $encryptionKey, $usersFile;
if (!file_exists($usersFile)) return null;
foreach (file($usersFile, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
$parts = explode(':', trim($line));
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
return decryptData($parts[3], $encryptionKey);
}
}
return null;
}
// Reuse the same authentication function
function authenticate($username, $password)
{
global $usersFile;
if (!file_exists($usersFile)) {
error_log("authenticate(): users file not found");
return false;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
if ($username === $storedUser && password_verify($password, $storedPass)) {
return $storedRole; // Return the user's role
}
}
error_log("authenticate(): authentication failed for '$username'");
return false;
}
// Define helper function to get a user's role from users.txt
function getUserRole($username) {
global $usersFile;
if (file_exists($usersFile)) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(":", trim($line));
if (count($parts) >= 3 && $parts[0] === $username) {
return trim($parts[2]);
}
}
}
return null;
}
// Add the loadFolderPermission function here:
function loadFolderPermission($username) {
global $encryptionKey;
$permissionsFile = USERS_DIR . 'userPermissions.json';
if (file_exists($permissionsFile)) {
$content = file_get_contents($permissionsFile);
$decrypted = decryptData($content, $encryptionKey);
$permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true);
if (is_array($permissions)) {
foreach ($permissions as $storedUsername => $data) {
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
return (bool)$data['folderOnly'];
}
}
}
}
return false;
}
// Check if the user has sent 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 (optional)
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
$roleFromAuth = authenticate($username, $password);
if ($roleFromAuth !== false) {
// --- NEW: check for TOTP secret ---
$secret = getUserTOTPSecret($username);
if ($secret) {
// hold user & secret in session and ask client for TOTP
$_SESSION['pending_login_user'] = $username;
$_SESSION['pending_login_secret'] = $secret;
header("Location: index.html?totp_required=1");
exit;
}
// no TOTP, proceed as before
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = (getUserRole($username) === "1");
$_SESSION["folderOnly"] = loadFolderPermission($username);
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;
?>

View File

@@ -1,50 +0,0 @@
<?php
require_once 'config.php';
// Retrieve headers and check CSRF token.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
// Log CSRF mismatch but proceed with 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.
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, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (is_array($persistentTokens) && isset($persistentTokens[$token])) {
unset($persistentTokens[$token]);
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
}
}
// Clear the cookie.
// Ensure $secure is defined; for example:
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
}
// Clear session data and remove session cookie.
$_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();
header("Location: index.html?logout=1");
exit;
?>

View File

@@ -1,164 +0,0 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to move files."]);
exit();
}
}
$data = json_decode(file_get_contents("php://input"), true);
if (
!$data ||
!isset($data['source']) ||
!isset($data['destination']) ||
!isset($data['files'])
) {
echo json_encode(["error" => "Invalid request"]);
exit;
}
$sourceFolder = trim($data['source']) ?: 'root';
$destinationFolder = trim($data['destination']) ?: 'root';
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
$folderPattern = REGEX_FOLDER_NAME;
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
}
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
echo json_encode(["error" => "Invalid destination folder name."]);
exit;
}
// Remove any leading/trailing slashes.
$sourceFolder = trim($sourceFolder, "/\\ ");
$destinationFolder = trim($destinationFolder, "/\\ ");
// Build the source and destination directories.
$baseDir = rtrim(UPLOAD_DIR, '/\\');
$sourceDir = ($sourceFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
// Ensure destination directory exists.
if (!is_dir($destDir)) {
if (!mkdir($destDir, 0775, true)) {
echo json_encode(["error" => "Could not create destination folder"]);
exit;
}
}
// Helper: Generate the metadata file path for a given folder.
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
// Helper: Generate a unique file name if a file with the same name exists.
function getUniqueFileName($destDir, $fileName) {
$fullPath = $destDir . $fileName;
clearstatcache(true, $fullPath);
if (!file_exists($fullPath)) {
return $fileName;
}
$basename = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$counter = 1;
do {
$newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : "");
$newFullPath = $destDir . $newName;
clearstatcache(true, $newFullPath);
$counter++;
} while (file_exists($destDir . $newName));
return $newName;
}
// Prepare metadata files.
$srcMetaFile = getMetadataFilePath($sourceFolder);
$destMetaFile = getMetadataFilePath($destinationFolder);
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($data['files'] as $fileName) {
// Save the original name for metadata lookup.
$originalName = basename(trim($fileName));
$basename = $originalName; // Start with the original name.
// Validate the file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has invalid characters.";
continue;
}
$srcPath = $sourceDir . $originalName;
$destPath = $destDir . $basename;
clearstatcache();
if (!file_exists($srcPath)) {
$errors[] = "$originalName does not exist in source.";
continue;
}
// If a file with the same name exists in destination, generate a unique name.
if (file_exists($destPath)) {
$uniqueName = getUniqueFileName($destDir, $basename);
$basename = $uniqueName;
$destPath = $destDir . $uniqueName;
}
if (!rename($srcPath, $destPath)) {
$errors[] = "Failed to move $basename";
continue;
}
// Update metadata: if there is metadata for the original file, move it under the new name.
if (isset($srcMetadata[$originalName])) {
$destMetadata[$basename] = $srcMetadata[$originalName];
unset($srcMetadata[$originalName]);
}
}
if (file_put_contents($srcMetaFile, json_encode($srcMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update source metadata.";
}
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update destination metadata.";
}
if (empty($errors)) {
echo json_encode(["success" => "Files moved successfully"]);
} else {
echo json_encode(["error" => implode("; ", $errors)]);
}
?>

8
public/api/addUser.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
// public/api/addUser.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->addUser();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/admin/getConfig.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
$adminController = new AdminController();
$adminController->getConfig();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/admin/updateConfig.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
$adminController = new AdminController();
$adminController->updateConfig();

9
public/api/auth/auth.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
// public/api/auth/auth.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/vendor/autoload.php';
require_once PROJECT_ROOT . '/src/controllers/authController.php';
$authController = new AuthController();
$authController->auth();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/auth/checkAuth.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/authController.php';
$authController = new AuthController();
$authController->checkAuth();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/auth/login_basic.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/authController.php';
$authController = new AuthController();
$authController->loginBasic();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/auth/logout.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/authController.php';
$authController = new AuthController();
$authController->logout();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/auth/token.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/authController.php';
$authController = new AuthController();
$authController->getToken();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/changePassword.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->changePassword();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/copyFiles.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->copyFiles();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/createShareLink.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->createShareLink();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/deleteFiles.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->deleteFiles();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/deleteTrashFiles.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->deleteTrashFiles();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/download.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->downloadFile();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/downloadZip.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->downloadZip();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/extractZip.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->extractZip();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/getFileList.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->getFileList();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/getFileTag.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->getFileTags();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/getTrashItems.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->getTrashItems();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/moveFiles.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->moveFiles();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/renameFile.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->renameFile();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/restoreFiles.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->restoreFiles();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/saveFile.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->saveFile();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/saveFileTag.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->saveFileTag();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/file/share.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
$fileController = new FileController();
$fileController->shareFile();

2
public/api/file/symlink Normal file
View File

@@ -0,0 +1,2 @@
cd /var/www/public
ln -s ../uploads uploads

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/createFolder.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->createFolder();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/createShareFolderLink.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->createShareFolderLink();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/deleteFolder.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->deleteFolder();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/downloadSharedFile.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->downloadSharedFile();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/getFolderList.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->getFolderList();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/renameFolder.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->renameFolder();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/shareFolder.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->shareFolder();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/folder/uploadToSharedFolder.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
$folderController = new FolderController();
$folderController->uploadToSharedFolder();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/getUserPermissions.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->getUserPermissions();

8
public/api/getUsers.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
// public/api/getUsers.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->getUsers(); // This will output the JSON response

View File

@@ -0,0 +1,8 @@
<?php
// public/api/removeUser.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->removeUser();

View File

@@ -0,0 +1,9 @@
<?php
// public/api/totp_disable.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/vendor/autoload.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->disableTOTP();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/totp_recover.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->recoverTOTP();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/totp_saveCode.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->saveTOTPRecoveryCode();

View File

@@ -0,0 +1,9 @@
<?php
// public/api/totp_setup.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/vendor/autoload.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->setupTOTP();

View File

@@ -0,0 +1,9 @@
<?php
// public/api/totp_verify.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/vendor/autoload.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->verifyTOTP();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/updateUserPanel.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->updateUserPanel();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/updateUserPermissions.php
require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/userController.php';
$userController = new UserController();
$userController->updateUserPermissions();

View File

@@ -0,0 +1,8 @@
<?php
// public/api/upload/removeChunks.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
$uploadController = new UploadController();
$uploadController->removeChunks();

View File

@@ -0,0 +1,7 @@
<?php
// public/api/upload/upload.php
require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
$uploadController = new UploadController();
$uploadController->handleUpload();

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1711,6 +1711,20 @@ body.dark-mode .CodeMirror-matchingbracket {
border-bottom: 1px solid #ffffff !important;
}
.zoom_in,
.zoom_out,
.rotate_left,
.rotate_right {
background: rgba(80, 80, 80, 0.6) !important;
border: none !important;
color: white !important;
cursor: pointer !important;
border-radius: 4px !important;
transition: background 0.3s ease, box-shadow 0.3s ease !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
transform: translateY(-10px);
}
.gallery-nav-btn {
background: rgba(80, 80, 80, 0.6) !important;
border: none !important;
@@ -1723,21 +1737,15 @@ body.dark-mode .CodeMirror-matchingbracket {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
}
.gallery-nav-btn:hover {
.gallery-nav-btn:hover,
.zoom_in:hover,
.zoom_out:hover,
.rotate_left:hover,
.rotate_right:hover {
background: rgba(80, 80, 80, 0.8) !important;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important;
}
.gallery-nav-btn.left {
left: 10px;
right: auto;
}
.gallery-nav-btn.right {
right: 10px;
left: auto;
}
.drop-hover {
background-color: #e0e0e0;
border: 1px dashed #666;

View File

@@ -200,7 +200,7 @@
</div>
<!-- Basic HTTP Login Option -->
<div class="text-center mt-3">
<a href="login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
Login</a>
</div>
</div>

View File

@@ -25,7 +25,7 @@ const currentOIDCConfig = {
providerUrl: "https://your-oidc-provider.com",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
redirectUri: "https://yourdomain.com/api/auth/auth.php?oidc=callback",
globalOtpauthUrl: ""
};
window.currentOIDCConfig = currentOIDCConfig;
@@ -51,7 +51,7 @@ function openTOTPLoginModal() {
const isFormLogin = Boolean(window.__lastLoginData);
if (!isFormLogin) {
// disable BasicAuth link
const basicLink = document.querySelector("a[href='login_basic.php']");
const basicLink = document.querySelector("a[href='/api/auth/login_basic.php']");
if (basicLink) {
basicLink.style.pointerEvents = 'none';
basicLink.style.opacity = '0.5';
@@ -78,8 +78,9 @@ function updateItemsPerPageSelect() {
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
const authForm = document.getElementById("authForm");
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
@@ -93,20 +94,18 @@ function updateLoginOptionsUIFromStorage() {
});
}
function loadAdminConfigFunc() {
return fetch("getConfig.php", { credentials: "include" })
export function loadAdminConfigFunc() {
return fetch("api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
// Save header_title into localStorage (if needed)
localStorage.setItem("headerTitle", config.header_title || "FileRise");
// Update login options and global OTPAuth URL as before
// Update login options using the nested loginOptions object.
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
// Update the UI for login options
updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1");
@@ -115,7 +114,7 @@ function loadAdminConfigFunc() {
}
})
.catch(() => {
// Fallback defaults in case of error
// Use defaults.
localStorage.setItem("headerTitle", "FileRise");
localStorage.setItem("disableFormLogin", "false");
localStorage.setItem("disableBasicAuth", "false");
@@ -150,11 +149,12 @@ function updateAuthenticatedUI(data) {
if (data.username) {
localStorage.setItem("username", data.username);
}
/*
if (typeof data.folderOnly !== "undefined") {
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
if (typeof data.folderOnly !== "undefined") {
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
}
*/
const headerButtons = document.querySelector(".header-buttons");
const firstButton = headerButtons.firstElementChild;
@@ -214,7 +214,7 @@ function updateAuthenticatedUI(data) {
}
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
return sendRequest("api/auth/checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
@@ -228,6 +228,10 @@ function checkAuthentication(showLoginToast = true) {
}
window.setupMode = false;
if (data.authenticated) {
localStorage.setItem("folderOnly", data.folderOnly );
localStorage.setItem("readOnly", data.readOnly );
localStorage.setItem("disableUpload",data.disableUpload);
updateLoginOptionsUIFromStorage();
if (typeof data.totp_enabled !== "undefined") {
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
}
@@ -250,12 +254,13 @@ function checkAuthentication(showLoginToast = true) {
function submitLogin(data) {
setLastLoginData(data);
window.__lastLoginData = data;
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success || response.status === "ok") {
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
// Fetch and update permissions, then reload.
sendRequest("getUserPermissions.php", "GET")
sendRequest("api/getUserPermissions.php", "GET")
.then(permissionData => {
if (permissionData && typeof permissionData === "object") {
localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false");
@@ -264,7 +269,7 @@ function submitLogin(data) {
}
})
.catch(() => {
// if fetching permissions fails.
// ignore permissionfetch errors
})
.finally(() => {
window.location.reload();
@@ -273,7 +278,7 @@ function submitLogin(data) {
openTOTPLoginModal();
} else if (response.error && response.error.includes("Too many failed login attempts")) {
showToast(response.error);
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
const loginButton = document.querySelector("#authForm button[type='submit']");
if (loginButton) {
loginButton.disabled = true;
setTimeout(() => {
@@ -285,10 +290,18 @@ function submitLogin(data) {
showToast("Login failed: " + (response.error || "Unknown error"));
}
})
.catch(() => {
showToast("Login failed: Unknown error");
.catch(err => {
// err may be an Error object or a string
let msg = "Unknown error";
if (err && typeof err === "object") {
msg = err.error || err.message || msg;
} else if (typeof err === "string") {
msg = err;
}
showToast(`Login failed: ${msg}`);
});
}
window.submitLogin = submitLogin;
/* ----------------- Other Helpers ----------------- */
@@ -313,9 +326,11 @@ function closeRemoveUserModal() {
}
function loadUserList() {
fetch("getUsers.php", { credentials: "include" })
// Updated path: from "getUsers.php" to "api/getUsers.php"
fetch("api/getUsers.php", { credentials: "include" })
.then(response => response.json())
.then(data => {
// Assuming the endpoint returns an array of users.
const users = Array.isArray(data) ? data : (data.users || []);
const selectElem = document.getElementById("removeUsernameSelect");
selectElem.innerHTML = "";
@@ -330,7 +345,7 @@ function loadUserList() {
closeRemoveUserModal();
}
})
.catch(() => { });
.catch(() => { /* handle errors if needed */ });
}
window.loadUserList = loadUserList;
@@ -353,7 +368,7 @@ function initAuth() {
});
}
document.getElementById("logoutBtn").addEventListener("click", function () {
fetch("logout.php", {
fetch("api/auth/logout.php", {
method: "POST",
credentials: "include",
headers: { "X-CSRF-Token": window.csrfToken }
@@ -372,7 +387,7 @@ function initAuth() {
showToast("Username and password are required!");
return;
}
let url = "addUser.php";
let url = "api/addUser.php";
if (window.setupMode) url += "?setup=1";
fetch(url, {
method: "POST",
@@ -407,7 +422,7 @@ function initAuth() {
}
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
if (!confirmed) return;
fetch("removeUser.php", {
fetch("api/removeUser.php", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
@@ -446,7 +461,7 @@ function initAuth() {
return;
}
const data = { oldPassword, newPassword, confirmPassword };
fetch("changePassword.php", {
fetch("api/changePassword.php", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
@@ -479,7 +494,7 @@ document.addEventListener("DOMContentLoaded", function () {
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) {
oidcLoginBtn.addEventListener("click", () => {
window.location.href = "auth.php?oidc=initiate";
window.location.href = "/api/auth/auth.php?oidc=initiate";
});
}

View File

@@ -1,8 +1,9 @@
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js';
import { loadAdminConfigFunc } from './auth.js';
const version = "v1.1.3";
const version = "v1.2.0";
// Use t() for the admin panel title. (Make sure t("admin_panel") returns "Admin Panel" in English.)
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
@@ -83,7 +84,7 @@ export function openTOTPLoginModal() {
showToast(t("please_enter_recovery_code"));
return;
}
fetch("totp_recover.php", {
fetch("api/totp_recover.php", {
method: "POST",
credentials: "include",
headers: {
@@ -96,7 +97,7 @@ export function openTOTPLoginModal() {
.then(json => {
if (json.status === "ok") {
// recovery succeeded → finalize login
window.location.href = "index.html";
window.location.href = "/index.html";
} else {
showToast(json.message || t("recovery_code_verification_failed"));
}
@@ -112,7 +113,7 @@ export function openTOTPLoginModal() {
totpInput.addEventListener("input", function () {
const code = this.value.trim();
if (code.length === 6) {
fetch("totp_verify.php", {
fetch("api/totp_verify.php", {
method: "POST",
credentials: "include",
headers: {
@@ -124,7 +125,7 @@ export function openTOTPLoginModal() {
.then(res => res.json())
.then(json => {
if (json.status === "ok") {
window.location.href = "index.html";
window.location.href = "/index.html";
} else {
showToast(json.message || t("totp_verification_failed"));
this.value = "";
@@ -228,7 +229,7 @@ export function openUserPanel() {
totpCheckbox.addEventListener("change", function () {
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
const enabled = this.checked;
fetch("updateUserPanel.php", {
fetch("api/updateUserPanel.php", {
method: "POST",
credentials: "include",
headers: {
@@ -352,7 +353,7 @@ export function openTOTPModal() {
showToast(t("please_enter_valid_code"));
return;
}
fetch("totp_verify.php", {
fetch("api/totp_verify.php", {
method: "POST",
credentials: "include",
headers: {
@@ -366,7 +367,7 @@ export function openTOTPModal() {
if (result.status === 'ok') {
showToast(t("totp_enabled_successfully"));
// After successful TOTP verification, fetch the recovery code
fetch("totp_saveCode.php", {
fetch("api/totp_saveCode.php", {
method: "POST",
credentials: "include",
headers: {
@@ -430,7 +431,7 @@ export function openTOTPModal() {
}
function loadTOTPQRCode() {
fetch("totp_setup.php", {
fetch("api/totp_setup.php", {
method: "GET",
credentials: "include",
headers: {
@@ -469,7 +470,7 @@ export function closeTOTPModal(disable = true) {
localStorage.setItem("userTOTPEnabled", "false");
}
// Call endpoint to remove the TOTP secret from the user's record
fetch("totp_disable.php", {
fetch("api/totp_disable.php", {
method: "POST",
credentials: "include",
headers: {
@@ -555,7 +556,7 @@ function showCustomConfirmModal(message) {
}
export function openAdminPanel() {
fetch("getConfig.php", { credentials: "include" })
fetch("api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
if (config.header_title) {
@@ -687,6 +688,7 @@ export function openAdminPanel() {
openUserPermissionsModal();
});
document.getElementById("saveAdminSettings").addEventListener("click", () => {
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
@@ -705,6 +707,7 @@ export function openAdminPanel() {
return;
}
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
const newOIDCConfig = {
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
clientId: document.getElementById("oidcClientId").value.trim(),
@@ -715,7 +718,7 @@ export function openAdminPanel() {
const disableBasicAuth = disableBasicAuthCheckbox.checked;
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
sendRequest("updateConfig.php", "POST", {
sendRequest("api/admin/updateConfig.php", "POST", {
header_title: newHeaderTitle,
oidc: newOIDCConfig,
disableFormLogin,
@@ -735,6 +738,7 @@ export function openAdminPanel() {
// Update the captured initial state since the changes have now been saved.
captureInitialAdminConfig();
closeAdminPanel();
loadAdminConfigFunc();
} else {
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
@@ -887,7 +891,7 @@ export function openUserPermissionsModal() {
});
});
// Send the permissionsData to the server.
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
sendRequest("api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast(t("user_permissions_updated_successfully"));
@@ -913,11 +917,11 @@ function loadUserPermissionsList() {
listContainer.innerHTML = "";
// First, fetch the current permissions from the server.
fetch("getUserPermissions.php", { credentials: "include" })
fetch("api/getUserPermissions.php", { credentials: "include" })
.then(response => response.json())
.then(permissionsData => {
// Then, fetch the list of users.
return fetch("getUsers.php", { credentials: "include" })
return fetch("api/getUsers.php", { credentials: "include" })
.then(response => response.json())
.then(usersData => {
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);

View File

@@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", function () {
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
fetch("deleteFiles.php", {
fetch("api/file/deleteFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -111,7 +111,7 @@ export function confirmSingleDownload() {
// Build the URL for download.php using GET parameters.
const folder = window.currentFolder || "root";
const downloadURL = "download.php?folder=" + encodeURIComponent(folder) +
const downloadURL = "/api/file/download.php?folder=" + encodeURIComponent(folder) +
"&file=" + encodeURIComponent(window.singleFileToDownload);
fetch(downloadURL, {
@@ -178,7 +178,7 @@ export function handleExtractZipSelected(e) {
// Show the progress modal.
document.getElementById("downloadProgressModal").style.display = "block";
fetch("extractZip.php", {
fetch("api/file/extractZip.php", {
method: "POST",
credentials: "include",
headers: {
@@ -245,7 +245,7 @@ document.addEventListener("DOMContentLoaded", function () {
console.log("Download confirmed. Showing progress modal.");
document.getElementById("downloadProgressModal").style.display = "block";
const folder = window.currentFolder || "root";
fetch("downloadZip.php", {
fetch("api/file/downloadZip.php", {
method: "POST",
credentials: "include",
headers: {
@@ -309,7 +309,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
if (window.userFolderOnly) {
const username = localStorage.getItem("username") || "root";
try {
const response = await fetch("getFolderList.php?restricted=1");
const response = await fetch("api/folder/getFolderList.php?restricted=1");
let folders = await response.json();
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
folders = folders.map(item => item.folder);
@@ -339,7 +339,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
}
try {
const response = await fetch("getFolderList.php");
const response = await fetch("api/folder/getFolderList.php");
let folders = await response.json();
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
folders = folders.map(item => item.folder);
@@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () {
showToast("Error: Cannot copy files to the same folder.");
return;
}
fetch("copyFiles.php", {
fetch("api/file/copyFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () {
showToast("Error: Cannot move files to the same folder.");
return;
}
fetch("moveFiles.php", {
fetch("api/file/moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -514,7 +514,7 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
const folderUsed = window.fileFolder;
fetch("renameFile.php", {
fetch("api/file/renameFile.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -96,7 +96,7 @@ export function folderDropHandler(event) {
return;
}
if (!dragData || !dragData.fileName) return;
fetch("moveFiles.php", {
fetch("api/file/moveFiles.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -160,7 +160,7 @@ export function saveFile(fileName, folder) {
content: editor.getValue(),
folder: folderUsed
};
fetch("saveFile.php", {
fetch("api/file/saveFile.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -90,6 +90,13 @@ function toggleAdvancedSearch() {
renderFileTable(window.currentFolder);
}
window.imageCache = window.imageCache || {};
function cacheImage(imgElem, key) {
// Save the current src for future renders.
window.imageCache[key] = imgElem.src;
}
window.cacheImage = cacheImage;
/**
* --- Fuse.js Search Helper ---
* Uses Fuse.js to perform a fuzzy search on fileData.
@@ -98,28 +105,28 @@ function toggleAdvancedSearch() {
*/
function searchFiles(searchTerm) {
if (!searchTerm) return fileData;
// Define search keys.
let keys = [
{ name: 'name', weight: 0.1 },
{ name: 'uploader', weight: 0.1 },
{ name: 'tags.name', weight: 0.1 }
{ name: 'name', weight: 0.1 },
{ name: 'uploader', weight: 0.1 },
{ name: 'tags.name', weight: 0.1 }
];
if (window.advancedSearchEnabled) {
keys.push({ name: 'content', weight: 0.7 });
keys.push({ name: 'content', weight: 0.7 });
}
const options = {
keys: keys,
threshold: 0.4,
minMatchCharLength: 2,
ignoreLocation: true
keys: keys,
threshold: 0.4,
minMatchCharLength: 2,
ignoreLocation: true
};
const fuse = new Fuse(fileData, options);
let results = fuse.search(searchTerm);
return results.map(result => result.item);
}
}
/**
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
@@ -127,43 +134,43 @@ function searchFiles(searchTerm) {
export function createViewToggleButton() {
let toggleBtn = document.getElementById("toggleViewBtn");
if (!toggleBtn) {
toggleBtn = document.createElement("button");
toggleBtn.id = "toggleViewBtn";
toggleBtn.classList.add("btn", "btn-toggleview");
// Set initial icon and tooltip based on current view mode.
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
// Insert the button before the last button in the header.
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons && headerButtons.lastElementChild) {
headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild);
} else if (headerButtons) {
headerButtons.appendChild(toggleBtn);
}
toggleBtn = document.createElement("button");
toggleBtn.id = "toggleViewBtn";
toggleBtn.classList.add("btn", "btn-toggleview");
// Set initial icon and tooltip based on current view mode.
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
// Insert the button before the last button in the header.
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons && headerButtons.lastElementChild) {
headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild);
} else if (headerButtons) {
headerButtons.appendChild(toggleBtn);
}
}
toggleBtn.onclick = () => {
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder);
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder);
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
};
return toggleBtn;
}
}
export function formatFolderName(folder) {
if (folder === "root") return "(Root)";
@@ -186,11 +193,11 @@ export function loadFileList(folderParam) {
fileListContainer.style.visibility = "hidden";
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
return fetch("api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
.then(response => {
if (response.status === 401) {
showToast("Session expired. Please log in again.");
window.location.href = "logout.php";
window.location.href = "/api/auth/logout.php";
throw new Error("Unauthorized");
}
return response.json();
@@ -240,6 +247,7 @@ export function loadFileList(folderParam) {
// Render view based on the view mode.
if (window.viewMode === "gallery") {
renderGalleryView(folder);
updateFileActionButtons();
} else {
renderFileTable(folder);
}
@@ -276,7 +284,7 @@ export function renderFileTable(folder, container) {
// Use Fuse.js search via our helper function.
const filteredFiles = searchFiles(searchTerm);
const totalFiles = filteredFiles.length;
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
if (currentPage > totalPages) {
@@ -295,7 +303,7 @@ export function renderFileTable(folder, container) {
});
const combinedTopHTML = topControlsHTML;
let headerHTML = buildFileTableHeader(sortOrder);
const startIndex = (currentPage - 1) * itemsPerPageSetting;
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
@@ -385,28 +393,54 @@ export function renderFileTable(folder, container) {
bindFileListContextMenu();
}
/**
* Similarly, update renderGalleryView to accept an optional container.
*/
// A helper to compute the max image height based on the current column count.
function getMaxImageHeight() {
const columns = parseInt(window.galleryColumns || 3, 10);
return 150 * (7 - columns); // adjust the multiplier as needed.
}
export function renderGalleryView(folder, container) {
const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
// Use Fuse.js search for gallery view as well.
const filteredFiles = searchFiles(searchTerm);
const folderPath = folder === "root"
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;";
let galleryHTML = `<div class="gallery-container" style="${gridStyle}">`;
// Use the current global column value (default to 3).
const numColumns = window.galleryColumns || 3;
// --- Insert slider controls ---
const sliderHTML = `
<div class="gallery-slider" style="margin: 10px; text-align: center;">
<label for="galleryColumnsSlider" style="margin-right: 5px;">${t('columns')}:</label>
<input type="range" id="galleryColumnsSlider" min="1" max="6" value="${numColumns}" style="vertical-align: middle;">
<span id="galleryColumnsValue">${numColumns}</span>
</div>
`;
// Set up the grid container using the slider's current value.
const gridStyle = `display: grid; grid-template-columns: repeat(${numColumns}, 1fr); gap: 10px; padding: 10px;`;
// Build the gallery container HTML including the slider.
let galleryHTML = sliderHTML;
galleryHTML += `<div class="gallery-container" style="${gridStyle}">`;
filteredFiles.forEach((file) => {
let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
const cacheKey = folderPath + encodeURIComponent(file.name);
if (window.imageCache && window.imageCache[cacheKey]) {
thumbnail = `<img src="${window.imageCache[cacheKey]}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`;
} else {
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime();
thumbnail = `<img src="${imageUrl}" onload="cacheImage(this, '${cacheKey}')" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`;
}
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
} else {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
}
let tagBadgesHTML = "";
if (file.tags && file.tags.length > 0) {
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
@@ -415,50 +449,117 @@ export function renderGalleryView(folder, container) {
});
tagBadgesHTML += `</div>`;
}
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
${thumbnail}
</div>
<div class="gallery-info" style="margin-top: 5px;">
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span>
${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
<button type="button" class="btn btn-sm btn-success download-btn"
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
title="${t('download')}">
<i class="material-icons">file_download</i>
</button>
${file.editable ? `
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('Edit')}">
<i class="material-icons">edit</i>
</button>
` : ""}
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="${t('share')}">
<i class="material-icons">share</i>
</button>
galleryHTML += `
<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
${thumbnail}
</div>
</div>
</div>`;
<div class="gallery-info" style="margin-top: 5px;">
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span>
${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
<button type="button" class="btn btn-sm btn-success download-btn"
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
title="${t('download')}">
<i class="material-icons">file_download</i>
</button>
${file.editable ? `
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('Edit')}">
<i class="material-icons">edit</i>
</button>
` : ""}
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="${t('share')}">
<i class="material-icons">share</i>
</button>
</div>
</div>
</div>`;
});
galleryHTML += "</div>";
galleryHTML += "</div>"; // End gallery container.
fileListContent.innerHTML = galleryHTML;
// Re-apply slider constraints for the newly rendered slider.
updateSliderConstraints();
createViewToggleButton();
updateFileActionButtons();
// Attach share button event listeners.
document.querySelectorAll(".share-btn").forEach(btn => {
btn.addEventListener("click", e => {
e.stopPropagation();
const fileName = btn.getAttribute("data-file");
const file = fileData.find(f => f.name === fileName);
import('./filePreview.js').then(module => {
module.openShareModal(file, folder);
});
if (file) {
import('./filePreview.js').then(module => {
module.openShareModal(file, folder);
});
}
});
});
// --- Slider Event Listener ---
const slider = document.getElementById("galleryColumnsSlider");
if (slider) {
slider.addEventListener("input", function () {
const value = this.value;
document.getElementById("galleryColumnsValue").textContent = value;
window.galleryColumns = value;
const galleryContainer = document.querySelector(".gallery-container");
if (galleryContainer) {
galleryContainer.style.gridTemplateColumns = `repeat(${value}, 1fr)`;
}
const newMaxHeight = getMaxImageHeight();
document.querySelectorAll(".gallery-thumbnail").forEach(img => {
img.style.maxHeight = newMaxHeight + "px";
});
});
}
}
// Responsive slider constraints based on screen size.
function updateSliderConstraints() {
const slider = document.getElementById("galleryColumnsSlider");
if (!slider) return;
const width = window.innerWidth;
let min = 1;
let max;
// Set maximum based on screen size.
if (width < 600) { // small devices (phones)
max = 2;
} else if (width < 1024) { // medium devices
max = 3;
} else if (width < 1440) { // between medium and large devices
max = 4;
} else { // large devices and above
max = 6;
}
// Adjust the slider's current value if needed
let currentVal = parseInt(slider.value, 10);
if (currentVal > max) {
currentVal = max;
slider.value = max;
}
slider.min = min;
slider.max = max;
document.getElementById("galleryColumnsValue").textContent = currentVal;
// Update the grid layout based on the current slider value.
const galleryContainer = document.querySelector(".gallery-container");
if (galleryContainer) {
galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`;
}
}
window.addEventListener('load', updateSliderConstraints);
window.addEventListener('resize', updateSliderConstraints);
export function sortFiles(column, folder) {
if (sortOrder.column === column) {
sortOrder.ascending = !sortOrder.ascending;

View File

@@ -48,7 +48,7 @@ export function openShareModal(file, folder) {
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
const expiration = document.getElementById("shareExpiration").value;
const password = document.getElementById("sharePassword").value;
fetch("createShareLink.php", {
fetch("api/file/createShareLink.php", {
method: "POST",
credentials: "include",
headers: {
@@ -65,9 +65,7 @@ export function openShareModal(file, folder) {
.then(response => response.json())
.then(data => {
if (data.token) {
let shareEndpoint = document.querySelector('meta[name="share-url"]')
? document.querySelector('meta[name="share-url"]').getAttribute('content')
: (window.SHARE_URL || "share.php");
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
const displayDiv = document.getElementById("shareLinkDisplay");
const inputField = document.getElementById("shareLinkInput");
@@ -121,12 +119,10 @@ export function previewFile(fileUrl, fileName) {
mediaElements.forEach(media => {
media.pause();
if (media.tagName.toLowerCase() !== 'video') {
try {
media.currentTime = 0;
} catch(e) { }
try { media.currentTime = 0; } catch (e) { }
}
});
modal.style.display = "none";
modal.remove();
}
document.getElementById("closeFileModal").addEventListener("click", closeModal);
@@ -143,24 +139,84 @@ export function previewFile(fileUrl, fileName) {
const extension = fileName.split('.').pop().toLowerCase();
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName);
if (isImage) {
// Create the image element with default transform data.
const img = document.createElement("img");
img.src = fileUrl;
img.className = "image-modal-img";
img.style.maxWidth = "80vw";
img.style.maxHeight = "80vh";
container.appendChild(img);
img.style.transition = "transform 0.3s ease";
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.position = 'relative';
img.style.zIndex = '1';
// Filter gallery images for navigation.
const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name));
if (images.length > 1) {
modal.galleryImages = images;
modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName);
// Create a flex wrapper to hold left panel, center image, and right panel.
const wrapper = document.createElement('div');
wrapper.className = 'image-wrapper';
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.justifyContent = 'center';
wrapper.style.position = 'relative';
// --- Left Panel: Contains Zoom controls (top) and Prev button (bottom) ---
const leftPanel = document.createElement('div');
leftPanel.className = 'left-panel';
leftPanel.style.display = 'flex';
leftPanel.style.flexDirection = 'column';
leftPanel.style.justifyContent = 'space-between';
leftPanel.style.alignItems = 'center';
leftPanel.style.width = '60px';
leftPanel.style.height = '100%';
leftPanel.style.zIndex = '10';
// Top container for zoom buttons.
const leftTop = document.createElement('div');
leftTop.style.display = 'flex';
leftTop.style.flexDirection = 'column';
leftTop.style.gap = '4px';
// Zoom In button.
const zoomInBtn = document.createElement('button');
zoomInBtn.className = 'material-icons zoom_in';
zoomInBtn.title = 'Zoom In';
zoomInBtn.style.background = 'transparent';
zoomInBtn.style.border = 'none';
zoomInBtn.style.cursor = 'pointer';
zoomInBtn.textContent = 'zoom_in';
// Zoom Out button.
const zoomOutBtn = document.createElement('button');
zoomOutBtn.className = 'material-icons zoom_out';
zoomOutBtn.title = 'Zoom Out';
zoomOutBtn.style.background = 'transparent';
zoomOutBtn.style.border = 'none';
zoomOutBtn.style.cursor = 'pointer';
zoomOutBtn.textContent = 'zoom_out';
leftTop.appendChild(zoomInBtn);
leftTop.appendChild(zoomOutBtn);
leftPanel.appendChild(leftTop);
// Bottom container for prev button.
const leftBottom = document.createElement('div');
leftBottom.style.display = 'flex';
leftBottom.style.justifyContent = 'center';
leftBottom.style.alignItems = 'center';
leftBottom.style.width = '100%';
if (images.length > 1) {
const prevBtn = document.createElement("button");
prevBtn.textContent = "";
prevBtn.className = "gallery-nav-btn";
prevBtn.style.cssText = "position: absolute; top: 50%; left: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
prevBtn.style.background = 'transparent';
prevBtn.style.border = 'none';
prevBtn.style.color = 'white';
prevBtn.style.fontSize = '48px';
prevBtn.style.cursor = 'pointer';
prevBtn.addEventListener("click", function (e) {
e.stopPropagation();
// Safety check:
if (!modal.galleryImages || modal.galleryImages.length === 0) return;
modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length;
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
modal.querySelector("h4").textContent = newFile.name;
@@ -168,13 +224,82 @@ export function previewFile(fileUrl, fileName) {
? "uploads/"
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
// Reset transforms.
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.transform = 'scale(1) rotate(0deg)';
});
leftBottom.appendChild(prevBtn);
} else {
// Insert an empty placeholder for consistent layout.
leftBottom.innerHTML = '&nbsp;';
}
leftPanel.appendChild(leftBottom);
// --- Center Panel: Contains the image ---
const centerPanel = document.createElement('div');
centerPanel.className = 'center-image-container';
centerPanel.style.flexGrow = '1';
centerPanel.style.textAlign = 'center';
centerPanel.style.position = 'relative';
centerPanel.style.zIndex = '1';
centerPanel.appendChild(img);
// --- Right Panel: Contains Rotate controls (top) and Next button (bottom) ---
const rightPanel = document.createElement('div');
rightPanel.className = 'right-panel';
rightPanel.style.display = 'flex';
rightPanel.style.flexDirection = 'column';
rightPanel.style.justifyContent = 'space-between';
rightPanel.style.alignItems = 'center';
rightPanel.style.width = '60px';
rightPanel.style.height = '100%';
rightPanel.style.zIndex = '10';
// Top container for rotate buttons.
const rightTop = document.createElement('div');
rightTop.style.display = 'flex';
rightTop.style.flexDirection = 'column';
rightTop.style.gap = '4px';
// Rotate Left button.
const rotateLeftBtn = document.createElement('button');
rotateLeftBtn.className = 'material-icons rotate_left';
rotateLeftBtn.title = 'Rotate Left';
rotateLeftBtn.style.background = 'transparent';
rotateLeftBtn.style.border = 'none';
rotateLeftBtn.style.cursor = 'pointer';
rotateLeftBtn.textContent = 'rotate_left';
// Rotate Right button.
const rotateRightBtn = document.createElement('button');
rotateRightBtn.className = 'material-icons rotate_right';
rotateRightBtn.title = 'Rotate Right';
rotateRightBtn.style.background = 'transparent';
rotateRightBtn.style.border = 'none';
rotateRightBtn.style.cursor = 'pointer';
rotateRightBtn.textContent = 'rotate_right';
rightTop.appendChild(rotateLeftBtn);
rightTop.appendChild(rotateRightBtn);
rightPanel.appendChild(rightTop);
// Bottom container for next button.
const rightBottom = document.createElement('div');
rightBottom.style.display = 'flex';
rightBottom.style.justifyContent = 'center';
rightBottom.style.alignItems = 'center';
rightBottom.style.width = '100%';
if (images.length > 1) {
const nextBtn = document.createElement("button");
nextBtn.textContent = "";
nextBtn.className = "gallery-nav-btn";
nextBtn.style.cssText = "position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
nextBtn.style.background = 'transparent';
nextBtn.style.border = 'none';
nextBtn.style.color = 'white';
nextBtn.style.fontSize = '48px';
nextBtn.style.cursor = 'pointer';
nextBtn.addEventListener("click", function (e) {
e.stopPropagation();
// Safety check:
if (!modal.galleryImages || modal.galleryImages.length === 0) return;
modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length;
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
modal.querySelector("h4").textContent = newFile.name;
@@ -182,11 +307,63 @@ export function previewFile(fileUrl, fileName) {
? "uploads/"
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
// Reset transforms.
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.transform = 'scale(1) rotate(0deg)';
});
container.appendChild(prevBtn);
container.appendChild(nextBtn);
rightBottom.appendChild(nextBtn);
} else {
// Insert a placeholder so that center remains properly aligned.
rightBottom.innerHTML = '&nbsp;';
}
rightPanel.appendChild(rightBottom);
// Assemble panels into the wrapper.
wrapper.appendChild(leftPanel);
wrapper.appendChild(centerPanel);
wrapper.appendChild(rightPanel);
container.appendChild(wrapper);
// --- Set up zoom controls event listeners ---
zoomInBtn.addEventListener('click', function (e) {
e.stopPropagation();
let scale = parseFloat(img.dataset.scale) || 1;
scale += 0.1;
img.dataset.scale = scale;
img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)';
});
zoomOutBtn.addEventListener('click', function (e) {
e.stopPropagation();
let scale = parseFloat(img.dataset.scale) || 1;
scale = Math.max(0.1, scale - 0.1);
img.dataset.scale = scale;
img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)';
});
// Attach rotation control listeners (always present now).
rotateLeftBtn.addEventListener('click', function (e) {
e.stopPropagation();
let rotate = parseFloat(img.dataset.rotate) || 0;
rotate = (rotate - 90 + 360) % 360;
img.dataset.rotate = rotate;
img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)';
});
rotateRightBtn.addEventListener('click', function (e) {
e.stopPropagation();
let rotate = parseFloat(img.dataset.rotate) || 0;
rotate = (rotate + 90) % 360;
img.dataset.rotate = rotate;
img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)';
});
// Save gallery details if there is more than one image.
if (images.length > 1) {
modal.galleryImages = images;
modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName);
}
} else {
// Handle non-image file previews.
if (extension === "pdf") {
const embed = document.createElement("embed");
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
@@ -201,26 +378,21 @@ export function previewFile(fileUrl, fileName) {
video.src = fileUrl;
video.controls = true;
video.className = "image-modal-img";
const progressKey = 'videoProgress-' + fileUrl;
video.addEventListener("loadedmetadata", () => {
const savedTime = localStorage.getItem(progressKey);
if (savedTime) {
video.currentTime = parseFloat(savedTime);
}
});
video.addEventListener("timeupdate", () => {
localStorage.setItem(progressKey, video.currentTime);
});
video.addEventListener("ended", () => {
localStorage.removeItem(progressKey);
});
container.appendChild(video);
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) {
const audio = document.createElement("audio");
audio.src = fileUrl;
@@ -235,26 +407,19 @@ export function previewFile(fileUrl, fileName) {
modal.style.display = "flex";
}
// Added to preserve the original functionality.
// Preserve original functionality.
export function displayFilePreview(file, container) {
const actualFile = file.file || file;
// Validate that actualFile is indeed a File
if (!(actualFile instanceof File)) {
console.error("displayFilePreview called with an invalid file object");
return;
}
container.style.display = "inline-block";
// Clear the container safely without using innerHTML
while (container.firstChild) {
container.removeChild(container.firstChild);
}
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) {
const img = document.createElement("img");
// Set the image source using a Blob URL (this is considered safe)
img.src = URL.createObjectURL(actualFile);
img.classList.add("file-preview-img");
container.appendChild(img);

View File

@@ -261,7 +261,7 @@ function removeGlobalTag(tagName) {
// NEW: Save global tag removal to the server.
function saveGlobalTagRemoval(tagName) {
fetch("saveFileTag.php", {
fetch("api/file/saveFileTag.php", {
method: "POST",
credentials: "include",
headers: {
@@ -305,7 +305,7 @@ if (localStorage.getItem('globalTags')) {
// New function to load global tags from the server's persistent JSON.
export function loadGlobalTags() {
fetch("getFileTag.php", { credentials: "include" })
fetch("api/file/getFileTag.php", { credentials: "include" })
.then(response => {
if (!response.ok) {
// If the file doesn't exist, assume there are no global tags.
@@ -438,7 +438,7 @@ export function saveFileTags(file, deleteGlobal = false, tagToDelete = null) {
payload.deleteGlobal = true;
payload.tagToDelete = tagToDelete;
}
fetch("saveFileTag.php", {
fetch("api/file/saveFileTag.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -154,7 +154,7 @@ function breadcrumbDropHandler(e) {
}
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
fetch("moveFiles.php", {
fetch("api/file/moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -202,7 +202,7 @@ function checkUserFolderPermission() {
window.currentFolder = username;
return Promise.resolve(true);
}
return fetch("getUserPermissions.php", { credentials: "include" })
return fetch("api/getUserPermissions.php", { credentials: "include" })
.then(response => response.json())
.then(permissionsData => {
console.log("checkUserFolderPermission: permissionsData =", permissionsData);
@@ -302,7 +302,7 @@ function folderDropHandler(event) {
}
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
fetch("moveFiles.php", {
fetch("api/file/moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -353,7 +353,7 @@ export async function loadFolderTree(selectedFolder) {
}
// Build fetch URL.
let fetchUrl = 'getFolderList.php';
let fetchUrl = 'api/folder/getFolderList.php';
if (window.userFolderOnly) {
fetchUrl += '?restricted=1';
}
@@ -364,7 +364,7 @@ export async function loadFolderTree(selectedFolder) {
if (response.status === 401) {
console.error("Unauthorized: Please log in to view folders.");
showToast("Session expired. Please log in again.");
window.location.href = "logout.php";
window.location.href = "/api/auth/logout.php";
return;
}
let folderData = await response.json();
@@ -547,7 +547,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
showToast("CSRF token not loaded yet! Please try again.");
return;
}
fetch("renameFolder.php", {
fetch("api/folder/renameFolder.php", {
method: "POST",
credentials: "include",
headers: {
@@ -592,7 +592,7 @@ attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("deleteFolder.php", {
fetch("api/folder/deleteFolder.php", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -639,7 +639,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
fullFolderName = selectedFolder + "/" + folderInput;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("createFolder.php", {
fetch("api/folder/createFolder.php", {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -64,7 +64,7 @@ export function openFolderShareModal(folder) {
return;
}
// Post to the createFolderShareLink endpoint.
fetch("/createFolderShareLink.php", {
fetch("api/folder/createShareFolderLink.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -236,7 +236,8 @@ const translations = {
"please_save_recovery_code": "Please save this code securely. It will not be shown again and can only be used once.",
"ok": "OK",
"show": "Show",
"items_per_page": "items per page"
"items_per_page": "items per page",
"columns":"Columns"
},
es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
@@ -463,7 +464,8 @@ const translations = {
"save_permissions": "Guardar permisos",
"your_recovery_code": "Su código de recuperación",
"please_save_recovery_code": "Por favor, guarde este código de forma segura. No se mostrará de nuevo y solo podrá usarse una vez.",
"ok": "OK"
"ok": "OK",
"columns": "Columnas"
},
fr: {
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
@@ -690,7 +692,8 @@ const translations = {
"save_permissions": "Enregistrer les permissions",
"your_recovery_code": "Votre code de récupération",
"please_save_recovery_code": "Veuillez sauvegarder ce code en toute sécurité. Il ne sera plus affiché et ne pourra être utilisé qu'une seule fois.",
"ok": "OK"
"ok": "OK",
"columns": "Colonnes"
},
de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
@@ -928,7 +931,8 @@ const translations = {
"please_save_recovery_code": "Bitte speichern Sie diesen Code sicher. Er wird nicht erneut angezeigt und kann nur einmal verwendet werden.",
"ok": "OK",
"show": "Zeige",
"items_per_page": "elemente pro seite"
"items_per_page": "elemente pro seite",
"columns": "Spalten"
}
};

View File

@@ -2,7 +2,7 @@ import { sendRequest } from './networkUtils.js';
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
import { loadFolderTree } from './folderManager.js';
import { initUpload } from './upload.js';
import { initAuth, checkAuthentication } from './auth.js';
import { initAuth, checkAuthentication, loadAdminConfigFunc } from './auth.js';
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
@@ -14,7 +14,7 @@ import { t, applyTranslations, setLocale } from './i18n.js';
// Remove the retry logic version and just use loadCsrfToken directly:
function loadCsrfToken() {
return fetch('token.php', { credentials: 'include' })
return fetch('api/auth/token.php', { credentials: 'include' })
.then(response => {
if (!response.ok) {
throw new Error("Token fetch failed with status: " + response.status);
@@ -60,6 +60,8 @@ window.openDownloadModal = openDownloadModal;
window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () {
loadAdminConfigFunc(); // Then fetch the latest config and update.
// Retrieve the saved language from localStorage; default to "en"
const savedLanguage = localStorage.getItem("language") || "en";
// Set the locale based on the saved language
@@ -84,6 +86,7 @@ document.addEventListener("DOMContentLoaded", function () {
initUpload();
loadFolderTree();
setupTrashRestoreDelete();
loadAdminConfigFunc();
const helpBtn = document.getElementById("folderHelpBtn");
const helpTooltip = document.getElementById("folderHelpTooltip");

31
public/js/networkUtils.js Normal file
View File

@@ -0,0 +1,31 @@
// public/js/networkUtils.js
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
const options = {
method,
credentials: 'include',
headers: { ...customHeaders }
};
if (data && !(data instanceof FormData)) {
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
options.body = JSON.stringify(data);
} else if (data instanceof FormData) {
options.body = data;
}
return fetch(url, options)
.then(async res => {
const text = await res.text();
let payload;
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
if (!res.ok) {
// Reject with the parsed JSON (or raw text) so .catch(error) gets it
throw payload;
}
return payload;
});
}

View File

@@ -69,7 +69,7 @@ export function setupTrashRestoreDelete() {
showToast(t("no_trash_selected"));
return;
}
fetch("restoreFiles.php", {
fetch("api/file/restoreFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -109,7 +109,7 @@ export function setupTrashRestoreDelete() {
showToast(t("trash_empty"));
return;
}
fetch("restoreFiles.php", {
fetch("api/file/restoreFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -151,7 +151,7 @@ export function setupTrashRestoreDelete() {
return;
}
showConfirm("Are you sure you want to permanently delete the selected trash items?", () => {
fetch("deleteTrashFiles.php", {
fetch("api/file/deleteTrashFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -186,7 +186,7 @@ export function setupTrashRestoreDelete() {
if (deleteAllBtn) {
deleteAllBtn.addEventListener("click", () => {
showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => {
fetch("deleteTrashFiles.php", {
fetch("api/file/deleteTrashFiles.php", {
method: "POST",
credentials: "include",
headers: {
@@ -234,7 +234,7 @@ export function setupTrashRestoreDelete() {
* Loads trash items from the server and updates the restore modal list.
*/
export function loadTrashItems() {
fetch("getTrashItems.php", { credentials: "include" })
fetch("api/file/getTrashItems.php", { credentials: "include" })
.then(response => response.json())
.then(trashItems => {
const listContainer = document.getElementById("restoreFilesList");
@@ -271,7 +271,7 @@ export function loadTrashItems() {
* Automatically purges (permanently deletes) trash items older than 3 days.
*/
function autoPurgeOldTrash() {
fetch("getTrashItems.php", { credentials: "include" })
fetch("api/file/getTrashItems.php", { credentials: "include" })
.then(response => response.json())
.then(trashItems => {
const now = Date.now();
@@ -279,7 +279,7 @@ function autoPurgeOldTrash() {
const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays);
if (oldItems.length > 0) {
const files = oldItems.map(item => item.trashName);
fetch("deleteTrashFiles.php", {
fetch("api/file/deleteTrashFiles.php", {
method: "POST",
credentials: "include",
headers: {

View File

@@ -126,7 +126,7 @@ function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, int
// Prefix with "resumable_" to match your PHP regex.
params.append('folder', 'resumable_' + identifier);
params.append('csrf_token', csrfToken);
fetch('removeChunks.php', {
fetch('api/upload/removeChunks.php', {
method: 'POST',
credentials: 'include',
headers: {
@@ -405,10 +405,11 @@ const useResumable = true; // Enable resumable for file picker uploads
let resumableInstance;
function initResumableUpload() {
resumableInstance = new Resumable({
target: "upload.php",
target: "/api/upload/upload.php",
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks
simultaneousUploads: 3,
forceChunkSize: true,
testChunks: false,
throttleProgressCallbacks: 1,
headers: { "X-CSRF-Token": window.csrfToken }
@@ -663,7 +664,7 @@ function submitFiles(allFiles) {
}
});
xhr.open("POST", "upload.php", true);
xhr.open("POST", "api/upload/upload.php", true);
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
xhr.send(formData);
});

Some files were not shown because too many files have changed in this diff Show More