Compare commits

...

24 Commits

Author SHA1 Message Date
Ryan
d839b3ac1c Fix folder share gallery view link 2025-04-17 02:38:32 -04:00
Ryan
766458f707 Dockerfile, custom-php.ini & start.sh moved into main repo 2025-04-17 02:10:46 -04:00
Ryan
22cce5a898 Overhaul networkUtils and expand auth 2025-04-17 01:20:18 -04:00
Ryan
75d3bf5a9b Refactor fixes and adjustments 2025-04-16 17:15:59 -04:00
Ryan
4ec4ba832f Changes 4/16 Refactor API endpoints and modularize controllers and models 2025-04-16 13:04:36 -04:00
Ryan
97b67593bc remove 2025-04-16 11:43:29 -04:00
Ryan
ec5c3fc452 Refactor API endpoints and modularize controllers and models 2025-04-16 11:40:17 -04:00
Ryan
853d8835d9 Adjust Gallery View max columns based on screen size & Adjust headerTitle to update globally 2025-04-15 16:07:20 -04:00
Ryan
1d36d002c6 Force resumable chunk size & fix chunk cleanup 2025-04-14 16:58:12 -04:00
Ryan
844976ef89 Updated zoom-in, zoom-out, rotate-left & rotate-right buttons to match prev & next 2025-04-14 13:06:30 -04:00
Ryan
66e0d7ecbe filePreview enhancements and ensure gallery view toggle displays 2025-04-14 03:42:24 -04:00
Ryan
a5fbcdef88 Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns. 2025-04-14 00:29:30 -04:00
Ryan
a897d1734f Gallery view enhancements 2025-04-13 20:48:34 -04:00
Ryan
a9c4200827 Extend i18n support: Add new translation keys for Download and Share modals 2025-04-13 18:38:21 -04:00
Ryan
97559873dc Added translations and data attributes for almost all user-facing text 2025-04-13 15:22:52 -04:00
Ryan
0683b27534 New Admin section Header Settings to change Header Title. 2025-04-13 05:51:19 -04:00
Ryan
49c42e8096 RecoveryCodeModal i18n 2025-04-13 04:17:32 -04:00
Ryan
ed39e112a9 more i18n.js keys added 2025-04-13 03:55:51 -04:00
Ryan
25edab923a Decreased header size some more and clickable logo 2025-04-13 02:52:14 -04:00
Ryan
b8ae3c4402 Advanced/Basic search button as material icon on same row as search bar. 2025-04-12 16:31:05 -04:00
Ryan
fb537b1d61 Change search box text when enabling advanced search. 2025-04-12 15:36:23 -04:00
Ryan
90439022e3 Gallery View Toggle button moved to header 2025-04-12 15:16:59 -04:00
Ryan
b4c8738b8a advance search readme added 2025-04-12 14:18:14 -04:00
Ryan
e193bf9b13 Advanced Search Implementation 2025-04-12 14:16:52 -04:00
135 changed files with 10199 additions and 6245 deletions

View File

@@ -1,13 +1,147 @@
# 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.
- authModals.js fully updated with i18n.js keys.
- main.js added Dark & Light mode i18n.js keys.
- New Admin section Header Settings to change Header Title.
- Admin Panel confirm unsaved changes.
- 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
- **Fuse.js Integration for Indexed Real-Time Searching**
- **Added Fuse.js Library:** Included Fuse.js via a CDN `<script>` tag to leverage its clientside fuzzy search capabilities.
- **Created searchFiles Helper Function:** Introduced a new function that uses Fuse.js to build an index and perform fuzzy searches over file properties (file name, uploader, and nested tag names).
- **Transformed JSON Object to Array:** Updated the loadFileList() function to convert the returned file data into an array (if it isnt already) and assign file names from JSON keys.
- **Updated Rendering Functions:** Modified both renderFileTable() and renderGalleryView() to use the searchFiles() helper instead of a simple inarray .filter(). This ensures that every search—realtime by user input—is powered by Fuse.jss indexed search.
- **Enhanced Search Configuration:** Configured Fuse.js to search across multiple keys (file name, uploader, and tags) so that users can find files based on any of these properties.
- Moved Gallery view toggle button into header.
- Removed css entries that are not needed anymore for Gallery View Toggle.
- Change search box text when enabling advanced search.
- Advanced/Basic search button as material icon on same row as search bar.
### Advanced Search Implementation
- **Advanced Search Toggle:**
- Added a global toggle (`window.advancedSearchEnabled`) and a UI button to switch between basic and advanced search modes.
- The toggle button label changes between "Advanced Search" and "Basic Search" to reflect the active mode.
- **Fuse.js Integration Updates:**
- Modified the `searchFiles()` function to conditionally include the `"content"` key in the Fuse.js keys only when advanced search mode is enabled.
- Adjusted Fuse.js options by adding `ignoreLocation: true`, adjusting the `threshold`, and optionally assigning weights (e.g., a lower weight for `name` and a higher weight for `content`) to prioritize matches in file content.
- **Backend (PHP) Enhancements:**
- Updated **getFileList.php** to read the content of text-based files (e.g., `.txt`, `.html`, `.md`, etc.) using `file_get_contents()`.
- Added a `"content"` property to the JSON response for eligible files to allow for full-text search in advanced mode.
### Fuse.js Integration for Indexed Real-Time Searching**
- **Added Fuse.js Library:** Included Fuse.js via a CDN `<script>` tag to leverage its clientside fuzzy search capabilities.
- **Created searchFiles Helper Function:** Introduced a new function that uses Fuse.js to build an index and perform fuzzy searches over file properties (file name, uploader, and nested tag names).
- **Transformed JSON Object to Array:** Updated the loadFileList() function to convert the returned file data into an array (if it isnt already) and assign file names from JSON keys.
- **Updated Rendering Functions:** Modified both renderFileTable() and renderGalleryView() to use the searchFiles() helper instead of a simple inarray .filter(). This ensures that every search—realtime by user input—is powered by Fuse.jss indexed search.
- **Enhanced Search Configuration:** Configured Fuse.js to search across multiple keys (file name, uploader, and tags) so that users can find files based on any of these properties.
---

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

@@ -22,7 +22,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes tweak and save changes without leaving FileRise.
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags and quickly locate them using our advanced, indexed real-time search. The built-in search now leverages Fuse.js to provide fuzzy matching across file names, tags, and uploader fields—helping you find that “important” document even if you make a typo.
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags and locate them instantly using our indexed real-time search. Easily switch to Advanced Search mode to enable fuzzy matching not only across file names, tags, and uploader fields but also within the content of text files—helping you find that “important” document even if you make a typo or need to search deep within the file.
- 🔒 **User Authentication & User Permissions:** Secure your portal with username/password login. Supports multiple users create user accounts (admin UI provided) for family or team members. User permissions such as User “Folder Only” feature assigns each user a dedicated folder within the root directory, named after their username, restricting them from viewing or modifying other directories. User Read Only and Disable Upload are additional permissions. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security.

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,36 +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 globalOtpauthUrl is set
$config = json_decode($decryptedContent, true);
if (!isset($config['globalOtpauthUrl'])) {
$config['globalOtpauthUrl'] = "";
}
echo json_encode($config);
} else {
echo json_encode([
'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,103 +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.
* For "root", returns "root_metadata.json". Otherwise, 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';
}
$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));
}
$fileList[] = [
'name' => $file,
'modified' => $fileDateModified,
'uploaded' => $fileUploadedDate,
'size' => $fileSizeFormatted,
'uploader' => $fileUploader,
'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : []
];
}
// 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,611 +0,0 @@
/* i18n.js */
const translations = {
en: { /* English translations */
"please_log_in_to_continue": "Please log in to continue.",
"no_files_selected": "No files selected.",
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
"element_not_found": "Element with id \"{id}\" not found.",
"search_placeholder": "Search files, tags, or uploader...",
"file_name": "File Name",
"date_modified": "Date Modified",
"upload_date": "Upload Date",
"file_size": "File Size",
"uploader": "Uploader",
"enter_totp_code": "Enter TOTP Code",
"use_recovery_code_instead": "Use Recovery Code instead",
"enter_recovery_code": "Enter Recovery Code",
"editing": "Editing",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Save",
"close": "Close",
"no_files_found": "No files found.",
"switch_to_table_view": "Switch to Table View",
"switch_to_gallery_view": "Switch to Gallery View",
"share_file": "Share File",
"set_expiration": "Set Expiration:",
"password_optional": "Password (optional):",
"generate_share_link": "Generate Share Link",
"shareable_link": "Shareable Link:",
"copy_link": "Copy Link",
"tag_file": "Tag File",
"tag_name": "Tag Name:",
"tag_color": "Tag Color:",
"save_tag": "Save Tag",
"files_in": "Files in",
"light_mode": "Light Mode",
"dark_mode": "Dark Mode",
"upload_instruction": "Drop files/folders here or click 'Choose files'",
"no_files_selected_default": "No files selected",
"choose_files": "Choose files",
"delete_selected": "Delete Selected",
"copy_selected": "Copy Selected",
"move_selected": "Move Selected",
"tag_selected": "Tag Selected",
"download_zip": "Download Zip",
"extract_zip": "Extract Zip",
"preview": "Preview",
"edit": "Edit",
"rename": "Rename",
"trash_empty": "Trash is empty.",
"no_trash_selected": "No trash items selected for restore.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Logout",
"change_password": "Change Password",
"restore_text": "Restore or",
"delete_text": "Delete Trash Items",
"restore_selected": "Restore Selected",
"restore_all": "Restore All",
"delete_selected_trash": "Delete Selected",
"delete_all": "Delete All",
"upload_header": "Upload Files/Folders",
// Folder Management keys:
"folder_navigation": "Folder Navigation & Management",
"create_folder": "Create Folder",
"create_folder_title": "Create Folder",
"enter_folder_name": "Enter folder name",
"cancel": "Cancel",
"create": "Create",
"rename_folder": "Rename Folder",
"rename_folder_title": "Rename Folder",
"rename_folder_placeholder": "Enter new folder name",
"delete_folder": "Delete Folder",
"delete_folder_title": "Delete Folder",
"delete_folder_message": "Are you sure you want to delete this folder?",
"folder_help": "Folder Help",
"folder_help_item_1": "Click on a folder in the tree to view its files.",
"folder_help_item_2": "Use [-] to collapse and [+] to expand folders.",
"folder_help_item_3": "Select a folder and click \"Create Folder\" to add a subfolder.",
"folder_help_item_4": "To rename or delete a folder, select it and then click the appropriate button.",
// File List keys:
"file_list_title": "Files in (Root)",
"files_in": "Files in",
"delete_files": "Delete Files",
"delete_selected_files_title": "Delete Selected Files",
"delete_files_message": "Are you sure you want to delete the selected files?",
"copy_files": "Copy Files",
"copy_files_title": "Copy Selected Files",
"copy_files_message": "Select a target folder for copying the selected files:",
"move_files": "Move Files",
"move_files_title": "Move Selected Files",
"move_files_message": "Select a target folder for moving the selected files:",
"move": "Move",
"extract_zip_button": "Extract Zip",
"download_zip_title": "Download Selected Files as Zip",
"download_zip_prompt": "Enter a name for the zip file:",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Login",
"remember_me": "Remember me",
"login_oidc": "Login with OIDC",
"basic_http_login": "Use Basic HTTP Login",
// Change Password keys:
"change_password_title": "Change Password",
"old_password": "Old Password",
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
// Add User keys:
"create_new_user_title": "Create New User",
"username": "Username:",
"password": "Password:",
"grant_admin": "Grant Admin Access",
"save_user": "Save User",
// Remove User keys:
"remove_user_title": "Remove User",
"select_user_remove": "Select a user to remove:",
"delete_user": "Delete User",
// Rename File keys:
"rename_file_title": "Rename File",
"rename_file_placeholder": "Enter new file name",
// Folder Share
"share_folder": "Share Folder",
"allow_uploads": "Allow Uploads",
"share_link_generated": "Share Link Generated",
"error_generating_share_link": "Error Generating Share Link",
// Folder
"create_folder": "Create Folder",
"rename_folder": "Rename Folder",
"folder_share": "Share Folder",
"delete_folder": "Delete Folder",
// Custom Confirm Modal keys:
"yes": "Yes",
"no": "No",
"delete": "Delete",
"download": "Download",
"upload": "Upload",
"copy": "Copy",
"extract": "Extract",
"user": "User:",
"unknown_error": "Unknown Error",
"link_copied": "Link Copied to Clipboard",
"minutes": "minutes",
"hours": "hours",
"days": "days",
"weeks": "weeks",
"months": "months",
"seconds": "seconds",
// Dark Mode Toggle
"dark_mode_toggle": "Dark Mode",
"light_mode_toggle": "Light Mode"
},
es: { /* Spanish translations */
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
"no_files_selected": "No se seleccionaron archivos.",
"confirm_delete_files": "¿Está seguro de que desea eliminar {count} archivo(s) seleccionado(s)?",
"element_not_found": "Elemento con id \"{id}\" no encontrado.",
"search_placeholder": "Buscar archivos o etiqueta...",
"file_name": "Nombre del archivo",
"date_modified": "Fecha de modificación",
"upload_date": "Fecha de carga",
"file_size": "Tamaño del archivo",
"uploader": "Cargado por",
"enter_totp_code": "Ingrese el código TOTP",
"use_recovery_code_instead": "Usar código de recuperación en su lugar",
"enter_recovery_code": "Ingrese el código de recuperación",
"editing": "Editando",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Guardar",
"close": "Cerrar",
"no_files_found": "No se encontraron archivos.",
"switch_to_table_view": "Cambiar a vista de tabla",
"switch_to_gallery_view": "Cambiar a vista de galería",
"share_file": "Compartir archivo",
"set_expiration": "Establecer vencimiento:",
"password_optional": "Contraseña (opcional):",
"generate_share_link": "Generar enlace para compartir",
"shareable_link": "Enlace para compartir:",
"copy_link": "Copiar enlace",
"tag_file": "Etiquetar archivo",
"tag_name": "Nombre de la etiqueta:",
"tag_color": "Color de la etiqueta:",
"save_tag": "Guardar etiqueta",
"files_in": "Archivos en",
"light_mode": "Modo claro",
"dark_mode": "Modo oscuro",
"upload_instruction": "Suelte archivos/carpetas o haga clic en 'Elegir archivos'",
"no_files_selected_default": "No se seleccionaron archivos",
"choose_files": "Elegir archivos",
"delete_selected": "Eliminar seleccionados",
"copy_selected": "Copiar seleccionados",
"move_selected": "Mover seleccionados",
"tag_selected": "Etiquetar seleccionados",
"download_zip": "Descargar Zip",
"extract_zip": "Extraer Zip",
"preview": "Vista previa",
"edit": "Editar",
"rename": "Renombrar",
"trash_empty": "La papelera está vacía.",
"no_trash_selected": "No se seleccionaron elementos de la papelera para restaurar.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Cerrar sesión",
"change_password": "Cambiar contraseña",
"restore_text": "Restaurar o",
"delete_text": "Eliminar elementos de la papelera",
"restore_selected": "Restaurar seleccionados",
"restore_all": "Restaurar todo",
"delete_selected_trash": "Eliminar seleccionados",
"delete_all": "Eliminar todo",
"upload_header": "Cargar archivos/carpetas",
// Folder Management keys:
"folder_navigation": "Navegación y gestión de carpetas",
"create_folder": "Crear carpeta",
"create_folder_title": "Crear carpeta",
"enter_folder_name": "Ingrese el nombre de la carpeta",
"cancel": "Cancelar",
"create": "Crear",
"rename_folder": "Renombrar carpeta",
"rename_folder_title": "Renombrar carpeta",
"rename_folder_placeholder": "Ingrese el nuevo nombre de la carpeta",
"delete_folder": "Eliminar carpeta",
"delete_folder_title": "Eliminar carpeta",
"delete_folder_message": "¿Está seguro de que desea eliminar esta carpeta?",
"folder_help": "Ayuda de carpetas",
"folder_help_item_1": "Haga clic en una carpeta en el árbol para ver sus archivos.",
"folder_help_item_2": "Utilice [-] para colapsar y [+] para expandir carpetas.",
"folder_help_item_3": "Seleccione una carpeta y haga clic en \"Crear carpeta\" para agregar una subcarpeta.",
"folder_help_item_4": "Para renombrar o eliminar una carpeta, selecciónela y luego haga clic en el botón correspondiente.",
// File List keys:
"file_list_title": "Archivos en (Raíz)",
"delete_files": "Eliminar archivos",
"delete_selected_files_title": "Eliminar archivos seleccionados",
"delete_files_message": "¿Está seguro de que desea eliminar los archivos seleccionados?",
"copy_files": "Copiar archivos",
"copy_files_title": "Copiar archivos seleccionados",
"copy_files_message": "Seleccione una carpeta destino para copiar los archivos seleccionados:",
"move_files": "Mover archivos",
"move_files_title": "Mover archivos seleccionados",
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
"move": "Mover",
"extract_zip_button": "Extraer Zip",
"download_zip_title": "Descargar archivos seleccionados en Zip",
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
"zip_placeholder": "archivos.zip",
// Login Form keys:
"login": "Iniciar sesión",
"remember_me": "Recuérdame",
"login_oidc": "Iniciar sesión con OIDC",
"basic_http_login": "Usar autenticación HTTP básica",
// Change Password keys:
"change_password_title": "Cambiar contraseña",
"old_password": "Contraseña antigua",
"new_password": "Nueva contraseña",
"confirm_new_password": "Confirmar nueva contraseña",
// Add User keys:
"create_new_user_title": "Crear nuevo usuario",
"username": "Usuario:",
"password": "Contraseña:",
"grant_admin": "Otorgar acceso de administrador",
"save_user": "Guardar usuario",
// Remove User keys:
"remove_user_title": "Eliminar usuario",
"select_user_remove": "Seleccione un usuario para eliminar:",
"delete_user": "Eliminar usuario",
// Rename File keys:
"rename_file_title": "Renombrar archivo",
"rename_file_placeholder": "Ingrese el nuevo nombre del archivo",
// Custom Confirm Modal keys:
"yes": "Sí",
"no": "No",
"delete": "Eliminar",
"download": "Descargar",
"upload": "Cargar",
"copy": "Copiar",
"extract": "Extraer",
// Dark Mode Toggle
"dark_mode_toggle": "Modo oscuro"
},
fr: { /* French translations */
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
"no_files_selected": "Aucun fichier sélectionné.",
"confirm_delete_files": "Êtes-vous sûr de vouloir supprimer {count} fichier(s) sélectionné(s) ?",
"element_not_found": "Élément avec l'id \"{id}\" non trouvé.",
"search_placeholder": "Rechercher des fichiers ou un tag...",
"file_name": "Nom du fichier",
"date_modified": "Date de modification",
"upload_date": "Date de téléchargement",
"file_size": "Taille du fichier",
"uploader": "Téléversé par",
"enter_totp_code": "Entrez le code TOTP",
"use_recovery_code_instead": "Utilisez le code de récupération à la place",
"enter_recovery_code": "Entrez le code de récupération",
"editing": "Modification",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Enregistrer",
"close": "Fermer",
"no_files_found": "Aucun fichier trouvé.",
"switch_to_table_view": "Passer à la vue tableau",
"switch_to_gallery_view": "Passer à la vue galerie",
"share_file": "Partager le fichier",
"set_expiration": "Définir l'expiration :",
"password_optional": "Mot de passe (facultatif) :",
"generate_share_link": "Générer un lien de partage",
"shareable_link": "Lien partageable :",
"copy_link": "Copier le lien",
"tag_file": "Marquer le fichier",
"tag_name": "Nom du tag :",
"tag_color": "Couleur du tag :",
"save_tag": "Enregistrer le tag",
"files_in": "Fichiers dans",
"light_mode": "Mode clair",
"dark_mode": "Mode sombre",
"upload_instruction": "Déposez vos fichiers/dossiers ici ou cliquez sur 'Choisir des fichiers'",
"no_files_selected_default": "Aucun fichier sélectionné",
"choose_files": "Choisir des fichiers",
"delete_selected": "Supprimer la sélection",
"copy_selected": "Copier la sélection",
"move_selected": "Déplacer la sélection",
"tag_selected": "Marquer la sélection",
"download_zip": "Télécharger en Zip",
"extract_zip": "Extraire le Zip",
"preview": "Aperçu",
"edit": "Modifier",
"rename": "Renommer",
"trash_empty": "La corbeille est vide.",
"no_trash_selected": "Aucun élément de la corbeille sélectionné pour restauration.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Se déconnecter",
"change_password": "Changer le mot de passe",
"restore_text": "Restaurer ou",
"delete_text": "Supprimer les éléments de la corbeille",
"restore_selected": "Restaurer la sélection",
"restore_all": "Restaurer tout",
"delete_selected_trash": "Supprimer la sélection",
"delete_all": "Supprimer tout",
"upload_header": "Téléverser des fichiers/dossiers",
// Folder Management keys:
"folder_navigation": "Navigation et gestion des dossiers",
"create_folder": "Créer un dossier",
"create_folder_title": "Créer un dossier",
"enter_folder_name": "Entrez le nom du dossier",
"cancel": "Annuler",
"create": "Créer",
"rename_folder": "Renommer le dossier",
"rename_folder_title": "Renommer le dossier",
"rename_folder_placeholder": "Entrez le nouveau nom du dossier",
"delete_folder": "Supprimer le dossier",
"delete_folder_title": "Supprimer le dossier",
"delete_folder_message": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
"folder_help": "Aide des dossiers",
"folder_help_item_1": "Cliquez sur un dossier dans l'arborescence pour voir ses fichiers.",
"folder_help_item_2": "Utilisez [-] pour réduire et [+] pour développer les dossiers.",
"folder_help_item_3": "Sélectionnez un dossier et cliquez sur \"Créer un dossier\" pour ajouter un sous-dossier.",
"folder_help_item_4": "Pour renommer ou supprimer un dossier, sélectionnez-le puis cliquez sur le bouton approprié.",
// File List keys:
"file_list_title": "Fichiers dans (Racine)",
"delete_files": "Supprimer les fichiers",
"delete_selected_files_title": "Supprimer les fichiers sélectionnés",
"delete_files_message": "Êtes-vous sûr de vouloir supprimer les fichiers sélectionnés ?",
"copy_files": "Copier les fichiers",
"copy_files_title": "Copier les fichiers sélectionnés",
"copy_files_message": "Sélectionnez un dossier de destination pour copier les fichiers sélectionnés :",
"move_files": "Déplacer les fichiers",
"move_files_title": "Déplacer les fichiers sélectionnés",
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
"move": "Déplacer",
"extract_zip_button": "Extraire le Zip",
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
"zip_placeholder": "fichiers.zip",
// Login Form keys:
"login": "Connexion",
"remember_me": "Se souvenir de moi",
"login_oidc": "Connexion avec OIDC",
"basic_http_login": "Utiliser l'authentification HTTP basique",
// Change Password keys:
"change_password_title": "Changer le mot de passe",
"old_password": "Ancien mot de passe",
"new_password": "Nouveau mot de passe",
"confirm_new_password": "Confirmer le nouveau mot de passe",
// Add User keys:
"create_new_user_title": "Créer un nouvel utilisateur",
"username": "Nom d'utilisateur :",
"password": "Mot de passe :",
"grant_admin": "Accorder les droits d'administrateur",
"save_user": "Enregistrer l'utilisateur",
// Remove User keys:
"remove_user_title": "Supprimer l'utilisateur",
"select_user_remove": "Sélectionnez un utilisateur à supprimer :",
"delete_user": "Supprimer l'utilisateur",
// Rename File keys:
"rename_file_title": "Renommer le fichier",
"rename_file_placeholder": "Entrez le nouveau nom du fichier",
// Custom Confirm Modal keys:
"yes": "Oui",
"no": "Non",
"delete": "Supprimer",
"download": "Télécharger",
"upload": "Téléverser",
"copy": "Copier",
"extract": "Extraire",
// Dark Mode Toggle
"dark_mode_toggle": "Mode sombre"
},
de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
"no_files_selected": "Keine Dateien ausgewählt.",
"confirm_delete_files": "Sind Sie sicher, dass Sie {count} ausgewählte Datei(en) löschen möchten?",
"element_not_found": "Element mit der ID \"{id}\" wurde nicht gefunden.",
"search_placeholder": "Suche nach Dateien oder Tags...",
"file_name": "Dateiname",
"date_modified": "Änderungsdatum",
"upload_date": "Hochladedatum",
"file_size": "Dateigröße",
"uploader": "Hochgeladen von",
"enter_totp_code": "Geben Sie den TOTP-Code ein",
"use_recovery_code_instead": "Verwenden Sie stattdessen den Wiederherstellungscode",
"enter_recovery_code": "Geben Sie den Wiederherstellungscode ein",
"editing": "Bearbeitung",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Speichern",
"close": "Schließen",
"no_files_found": "Keine Dateien gefunden.",
"switch_to_table_view": "Zur Tabellenansicht wechseln",
"switch_to_gallery_view": "Zur Galerieansicht wechseln",
"share_file": "Datei teilen",
"set_expiration": "Ablauf festlegen:",
"password_optional": "Passwort (optional):",
"generate_share_link": "Freigabelink generieren",
"shareable_link": "Freigabelink:",
"copy_link": "Link kopieren",
"tag_file": "Datei taggen",
"tag_name": "Tagname:",
"tag_color": "Tagfarbe:",
"save_tag": "Tag speichern",
"files_in": "Dateien in",
"light_mode": "Heller Modus",
"dark_mode": "Dunkler Modus",
"upload_instruction": "Ziehen Sie Dateien/Ordner hierher oder klicken Sie auf 'Dateien auswählen'",
"no_files_selected_default": "Keine Dateien ausgewählt",
"choose_files": "Dateien auswählen",
"delete_selected": "Ausgewählte löschen",
"copy_selected": "Ausgewählte kopieren",
"move_selected": "Ausgewählte verschieben",
"tag_selected": "Ausgewählte taggen",
"download_zip": "Zip herunterladen",
"extract_zip": "Zip entpacken",
"preview": "Vorschau",
"edit": "Bearbeiten",
"rename": "Umbenennen",
"trash_empty": "Papierkorb ist leer.",
"no_trash_selected": "Keine Elemente im Papierkorb für die Wiederherstellung ausgewählt.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Abmelden",
"change_password": "Passwort ändern",
"restore_text": "Wiederherstellen oder",
"delete_text": "Papierkorbeinträge löschen",
"restore_selected": "Ausgewählte wiederherstellen",
"restore_all": "Alle wiederherstellen",
"delete_selected_trash": "Ausgewählte löschen",
"delete_all": "Alle löschen",
"upload_header": "Dateien/Ordner hochladen",
// Folder Management keys:
"folder_navigation": "Ordnernavigation & Verwaltung",
"create_folder": "Ordner erstellen",
"create_folder_title": "Ordner erstellen",
"enter_folder_name": "Geben Sie den Ordnernamen ein",
"cancel": "Abbrechen",
"create": "Erstellen",
"rename_folder": "Ordner umbenennen",
"rename_folder_title": "Ordner umbenennen",
"rename_folder_placeholder": "Neuen Ordnernamen eingeben",
"delete_folder": "Ordner löschen",
"delete_folder_title": "Ordner löschen",
"delete_folder_message": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
"folder_help": "Ordnerhilfe",
"folder_help_item_1": "Klicken Sie auf einen Ordner, um dessen Dateien anzuzeigen.",
"folder_help_item_2": "Verwenden Sie [-] um zu minimieren und [+] um zu erweitern.",
"folder_help_item_3": "Klicken Sie auf \"Ordner erstellen\", um einen Unterordner hinzuzufügen.",
"folder_help_item_4": "Um einen Ordner umzubenennen oder zu löschen, wählen Sie ihn und klicken Sie auf die entsprechende Schaltfläche.",
// File List keys:
"file_list_title": "Dateien in (Root)",
"delete_files": "Dateien löschen",
"delete_selected_files_title": "Ausgewählte Dateien löschen",
"delete_files_message": "Sind Sie sicher, dass Sie die ausgewählten Dateien löschen möchten?",
"copy_files": "Dateien kopieren",
"copy_files_title": "Ausgewählte Dateien kopieren",
"copy_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu kopieren:",
"move_files": "Dateien verschieben",
"move_files_title": "Ausgewählte Dateien verschieben",
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
"move": "Verschieben",
"extract_zip_button": "Zip entpacken",
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
"zip_placeholder": "dateien.zip",
// Login Form keys:
"login": "Anmelden",
"remember_me": "Angemeldet bleiben",
"login_oidc": "Mit OIDC anmelden",
"basic_http_login": "HTTP-Basisauthentifizierung verwenden",
// Change Password keys:
"change_password_title": "Passwort ändern",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort",
"confirm_new_password": "Neues Passwort bestätigen",
// Add User keys:
"create_new_user_title": "Neuen Benutzer erstellen",
"username": "Benutzername:",
"password": "Passwort:",
"grant_admin": "Admin-Rechte vergeben",
"save_user": "Benutzer speichern",
// Remove User keys:
"remove_user_title": "Benutzer entfernen",
"select_user_remove": "Wählen Sie einen Benutzer zum Entfernen:",
"delete_user": "Benutzer löschen",
// Rename File keys:
"rename_file_title": "Datei umbenennen",
"rename_file_placeholder": "Neuen Dateinamen eingeben",
// Custom Confirm Modal keys:
"yes": "Ja",
"no": "Nein",
"delete": "Löschen",
"download": "Herunterladen",
"upload": "Hochladen",
"copy": "Kopieren",
"extract": "Entpacken",
// Dark Mode Toggle
"dark_mode_toggle": "Dunkler Modus"
}
};
let currentLocale = 'en';
export function setLocale(locale) {
currentLocale = locale;
}
export function t(key, placeholders) {
const localeTranslations = translations[currentLocale] || {};
let translation = localeTranslations[key] || key;
if (placeholders) {
Object.keys(placeholders).forEach(ph => {
translation = translation.replace(`{${ph}}`, placeholders[ph]);
});
}
return translation;
}
export function applyTranslations() {
document.querySelectorAll('[data-i18n-key]').forEach(el => {
el.innerText = t(el.getAttribute('data-i18n-key'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder')));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.setAttribute('title', t(el.getAttribute('data-i18n-title')));
});
}

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

@@ -32,8 +32,8 @@ body {
@media (min-width: 1300px) {
.container-fluid {
padding-left: 40px !important;
padding-right: 40px !important;
padding-left: 30px !important;
padding-right: 30px !important;
}
}
@@ -69,7 +69,7 @@ body {
align-items: center;
justify-content: space-between;
width: 100%;
height: 65px;
height: 55px;
padding: 10px 20px;
background-color: #2196F3;
transition: background-color 0.3s ease;
@@ -82,28 +82,16 @@ body.dark-mode .header-container {
}
.header-logo {
max-height: 60px;
max-height: 50px;
width: auto;
display: block;
}
.header-logo svg {
height: 60px;
height: 50px;
width: auto;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
padding: 0 20px;
background-color: #2196F3;
transition: background-color 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
body.dark-mode header {
background-color: #1f1f1f;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
@@ -1584,39 +1572,6 @@ body.dark-mode .btn-secondary {
border-color: #6c757d;
}
#toggleViewBtn {
margin-bottom: 20px;
margin-left: 14px;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: background 0.3s ease, box-shadow 0.3s ease;
}
@media (max-width: 768px) {
#toggleViewBtn {
margin-left: 0 !important;
}
}
@media (max-width: 600px) {
#toggleViewBtn {
margin-left: 0 !important;
margin-right: auto !important;
display: block !important;
}
}
#toggleViewBtn:hover {
background: rgba(0, 0, 0, 0.8);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4);
}
body.dark-mode .btn-danger {
background-color: #dc3545;
color: #fff;
@@ -1729,21 +1684,6 @@ body.dark-mode .folder-help-icon {
}
body.dark-mode #searchIcon {
background-color: #444;
border: 1px solid #555;
border-radius: 4px;
color: #fff;
padding: 4px 8px;
}
body.dark-mode #searchInput {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
}
body.dark-mode .CodeMirror {
background: #1e1e1e !important;
color: #ffffff !important;
@@ -1771,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;
@@ -1783,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;
@@ -2161,3 +2109,57 @@ body.dark-mode .header-drop-zone.drag-active {
body.dark-mode #fileSummary {
color: white;
}
#searchIcon {
border-radius: 4px;
padding: 4px 8px;
}
body.dark-mode #searchIcon {
background-color: #444;
border: 1px solid #555;
color: #fff;
}
body.dark-mode #searchInput {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
}
.btn-icon {
background: transparent;
border: none;
padding: 6px 8px;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.btn-icon .material-icons,
#searchIcon .material-icons {
font-size: 24px;
line-height: 1;
margin: 0;
padding: 0;
color: #333;
}
.btn-icon:hover,
.btn-icon:focus {
background: rgba(0, 0, 0, 0.1);
outline: none;
}
body.dark-mode .btn-icon .material-icons,
body.dark-mode #searchIcon .material-icons {
color: #fff;
}
body.dark-mode .btn-icon:hover,
body.dark-mode .btn-icon:focus {
background: rgba(255, 255, 255, 0.1);
}

View File

@@ -50,69 +50,71 @@
<body>
<header class="header-container">
<div class="header-left">
<div class="header-logo">
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
<defs>
<!-- Gradient for the cabinet body -->
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
<!-- Drop shadow filter with animated attributes for a lifting effect -->
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
fill="freeze" />
</feDropShadow>
</filter>
</defs>
<style type="text/css">
/* Cabinet with gradient, white outline, and drop shadow */
.cabinet {
fill: url(#cabinetGradient);
stroke: white;
stroke-width: 2;
}
.divider {
stroke: #1565C0;
stroke-width: 1.5;
}
.drawer {
fill: #FFFFFF;
}
.handle {
fill: #1565C0;
}
</style>
<!-- Group that will animate upward and then back down once -->
<g id="cabinetGroup">
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
</g>
</svg>
</div>
<a href="index.html">
<div class="header-logo">
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
<defs>
<!-- Gradient for the cabinet body -->
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
<!-- Drop shadow filter with animated attributes for a lifting effect -->
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
fill="freeze" />
</feDropShadow>
</filter>
</defs>
<style type="text/css">
/* Cabinet with gradient, white outline, and drop shadow */
.cabinet {
fill: url(#cabinetGradient);
stroke: white;
stroke-width: 2;
}
.divider {
stroke: #1565C0;
stroke-width: 1.5;
}
.drawer {
fill: #FFFFFF;
}
.handle {
fill: #1565C0;
}
</style>
<!-- Group that will animate upward and then back down once -->
<g id="cabinetGroup">
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
</g>
</svg>
</div>
</a>
</div>
<div class="header-title">
<h1 data-i18n-key="header_title">FileRise</h1>
@@ -198,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>
@@ -389,29 +391,31 @@
</div> <!-- end mainColumn -->
</div> <!-- end main-wrapper -->
<!-- Download Progress Modal -->
<div id="downloadProgressModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<!-- Material icon spinner with a dedicated class -->
<span class="material-icons download-spinner">autorenew</span>
<p>Preparing your download...</p>
</div>
<!-- Download Progress Modal -->
<div id="downloadProgressModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<!-- Material icon spinner with a dedicated class -->
<span class="material-icons download-spinner">autorenew</span>
<p data-i18n-key="preparing_download">Preparing your download...</p>
</div>
</div>
<!-- Single File Download Modal -->
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4>Download File</h4>
<p>Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary"
onclick="document.getElementById('downloadFileModal').style.display = 'none';">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary"
onclick="confirmSingleDownload()">Download</button>
</div>
<!-- Single File Download Modal -->
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4 data-i18n-key="download_file">Download File</h4>
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary"
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
data-i18n-key="cancel">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary"
onclick="confirmSingleDownload()"
data-i18n-key="download">Download</button>
</div>
</div>
</div>
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
<div id="changePasswordModal" class="modal" style="display:none;">

View File

@@ -1,5 +1,5 @@
import { sendRequest } from './networkUtils.js';
import { t } from './i18n.js';
import { t, applyTranslations } from './i18n.js';
import {
toggleVisibility,
showToast as originalShowToast,
@@ -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,22 +94,38 @@ 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 => {
localStorage.setItem("headerTitle", config.header_title || "FileRise");
// 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");
updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) {
headerTitleElem.textContent = config.header_title || "FileRise";
}
})
.catch(() => {
// Use defaults.
localStorage.setItem("headerTitle", "FileRise");
localStorage.setItem("disableFormLogin", "false");
localStorage.setItem("disableBasicAuth", "false");
localStorage.setItem("disableOIDCLogin", "false");
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) {
headerTitleElem.textContent = "FileRise";
}
});
}
@@ -132,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;
@@ -146,7 +164,8 @@ function updateAuthenticatedUI(data) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
restoreBtn.setAttribute("data-i18n-title", "trash_restore_delete");
restoreBtn.innerHTML = '<i class="material-icons">restore_from_trash</i>';
if (firstButton) insertAfter(restoreBtn, firstButton);
else headerButtons.appendChild(restoreBtn);
}
@@ -157,7 +176,8 @@ function updateAuthenticatedUI(data) {
adminPanelBtn = document.createElement("button");
adminPanelBtn.id = "adminPanelBtn";
adminPanelBtn.classList.add("btn", "btn-info");
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
adminPanelBtn.setAttribute("data-i18n-title", "admin_panel");
adminPanelBtn.innerHTML = '<i class="material-icons">admin_panel_settings</i>';
insertAfter(adminPanelBtn, restoreBtn);
adminPanelBtn.addEventListener("click", openAdminPanel);
} else {
@@ -176,23 +196,25 @@ function updateAuthenticatedUI(data) {
userPanelBtn = document.createElement("button");
userPanelBtn.id = "userPanelBtn";
userPanelBtn.classList.add("btn", "btn-user");
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
userPanelBtn.setAttribute("data-i18n-title", "user_panel");
userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>';
const adminBtn = document.getElementById("adminPanelBtn");
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
else if (firstButton) insertAfter(userPanelBtn, firstButton);
else headerButtons.appendChild(userPanelBtn);
else headerButtons.appendChild(userPanelBtn);
userPanelBtn.addEventListener("click", openUserPanel);
} else {
userPanelBtn.style.display = "block";
}
}
applyTranslations();
updateItemsPerPageSelect();
updateLoginOptionsUIFromStorage();
}
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
return sendRequest("api/auth/checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
@@ -206,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");
}
@@ -228,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");
@@ -242,7 +269,7 @@ function submitLogin(data) {
}
})
.catch(() => {
// if fetching permissions fails.
// ignore permissionfetch errors
})
.finally(() => {
window.location.reload();
@@ -251,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(() => {
@@ -263,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 ----------------- */
@@ -291,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 = "";
@@ -308,7 +345,7 @@ function loadUserList() {
closeRemoveUserModal();
}
})
.catch(() => { });
.catch(() => { /* handle errors if needed */ });
}
window.loadUserList = loadUserList;
@@ -331,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 }
@@ -350,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",
@@ -385,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 },
@@ -424,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 },
@@ -457,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,9 +1,11 @@
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.2";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
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>`;
let lastLoginData = null;
export function setLastLoginData(data) {
@@ -44,7 +46,7 @@ export function openTOTPLoginModal() {
<input type="text" id="recoveryInput"
style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="Recovery code" />
<button type="button" id="submitRecovery" class="btn btn-secondary" style="margin-top:10px;">Submit Recovery Code</button>
<button type="button" id="submitRecovery" class="btn btn-secondary" style="margin-top:10px;">${t("submit_recovery_code")}</button>
</div>
</div>
`;
@@ -66,12 +68,12 @@ export function openTOTPLoginModal() {
// Switch to recovery
totpSection.style.display = "none";
recoverySection.style.display = "block";
toggleLink.textContent = "Use TOTP Code instead";
toggleLink.textContent = t("use_totp_code_instead");
} else {
// Switch back to TOTP
recoverySection.style.display = "none";
totpSection.style.display = "block";
toggleLink.textContent = "Use Recovery Code instead";
toggleLink.textContent = t("use_recovery_code_instead");
}
});
@@ -79,10 +81,10 @@ export function openTOTPLoginModal() {
document.getElementById("submitRecovery").addEventListener("click", () => {
const recoveryCode = document.getElementById("recoveryInput").value.trim();
if (!recoveryCode) {
showToast("Please enter your recovery code.");
showToast(t("please_enter_recovery_code"));
return;
}
fetch("totp_recover.php", {
fetch("api/totp_recover.php", {
method: "POST",
credentials: "include",
headers: {
@@ -95,13 +97,13 @@ 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 || "Recovery code verification failed");
showToast(json.message || t("recovery_code_verification_failed"));
}
})
.catch(() => {
showToast("Error verifying recovery code.");
showToast(t("error_verifying_recovery_code"));
});
});
@@ -111,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: {
@@ -123,16 +125,16 @@ 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 || "TOTP verification failed");
showToast(json.message || t("totp_verification_failed"));
this.value = "";
totpLoginModal.style.display = "flex";
totpInput.focus();
}
})
.catch(() => {
showToast("TOTP verification failed");
showToast(t("totp_verification_failed"));
this.value = "";
totpLoginModal.style.display = "flex";
totpInput.focus();
@@ -189,24 +191,24 @@ export function openUserPanel() {
userPanelModal.innerHTML = `
<div class="modal-content user-panel-content" style="${modalContentStyles}">
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>User Panel (${username})</h3>
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
<h3>${t("user_panel")} (${username})</h3>
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">${t("change_password")}</button>
<fieldset style="margin-bottom: 15px;">
<legend>TOTP Settings</legend>
<legend>${t("totp_settings")}</legend>
<div class="form-group">
<label for="userTOTPEnabled">Enable TOTP:</label>
<label for="userTOTPEnabled">${t("enable_totp")}:</label>
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Language</legend>
<legend>${t("language")}</legend>
<div class="form-group">
<label for="languageSelector">Select Language:</label>
<label for="languageSelector">${t("select_language")}:</label>
<select id="languageSelector">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="en">${t("english")}</option>
<option value="es">${t("spanish")}</option>
<option value="fr">${t("french")}</option>
<option value="de">${t("german")}</option>
</select>
</div>
</fieldset>
@@ -227,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: {
@@ -239,12 +241,12 @@ export function openUserPanel() {
.then(r => r.json())
.then(result => {
if (!result.success) {
showToast("Error updating TOTP setting: " + result.error);
showToast(t("error_updating_totp_setting") + ": " + result.error);
} else if (enabled) {
openTOTPModal();
}
})
.catch(() => { showToast("Error updating TOTP setting."); });
.catch(() => { showToast(t("error_updating_totp_setting")); });
});
// Language dropdown initialization
const languageSelector = document.getElementById("languageSelector");
@@ -283,10 +285,10 @@ function showRecoveryCodeModal(recoveryCode) {
`;
recoveryModal.innerHTML = `
<div style="background: #fff; color: #000; padding: 20px; max-width: 400px; width: 90%; border-radius: 8px; text-align: center;">
<h3>Your Recovery Code</h3>
<p>Please save this code securely. It will not be shown again and can only be used once.</p>
<h3>${t("your_recovery_code")}</h3>
<p>${t("please_save_recovery_code")}</p>
<code style="display: block; margin: 10px 0; font-size: 20px;">${recoveryCode}</code>
<button type="button" id="closeRecoveryModal" class="btn btn-primary">OK</button>
<button type="button" id="closeRecoveryModal" class="btn btn-primary">${t("ok")}</button>
</div>
`;
document.body.appendChild(recoveryModal);
@@ -327,15 +329,15 @@ export function openTOTPModal() {
totpModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>TOTP Setup</h3>
<p>Scan this QR code with your authenticator app:</p>
<h3>${t("totp_setup")}</h3>
<p>${t("scan_qr_code")}</p>
<!-- Create an image placeholder without the CSRF token in the src -->
<img id="totpQRCodeImage" src="" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
<br/>
<p>Enter the 6-digit code from your app to confirm setup:</p>
<p>${t("enter_totp_confirmation")}</p>
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
<br/><br/>
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">${t("confirm")}</button>
</div>
`;
document.body.appendChild(totpModal);
@@ -348,10 +350,10 @@ export function openTOTPModal() {
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
const code = document.getElementById("totpConfirmInput").value.trim();
if (code.length !== 6) {
showToast("Please enter a valid 6-digit code.");
showToast(t("please_enter_valid_code"));
return;
}
fetch("totp_verify.php", {
fetch("api/totp_verify.php", {
method: "POST",
credentials: "include",
headers: {
@@ -363,9 +365,9 @@ export function openTOTPModal() {
.then(r => r.json())
.then(result => {
if (result.status === 'ok') {
showToast("TOTP successfully enabled.");
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: {
@@ -379,16 +381,16 @@ export function openTOTPModal() {
// Show the recovery code in a secure modal
showRecoveryCodeModal(data.recoveryCode);
} else {
showToast("Error generating recovery code: " + (data.message || "Unknown error."));
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
}
})
.catch(() => { showToast("Error generating recovery code."); });
.catch(() => { showToast(t("error_generating_recovery_code")); });
closeTOTPModal(false);
} else {
showToast("TOTP verification failed: " + (result.message || "Invalid code."));
showToast(t("totp_verification_failed") + ": " + (result.message || t("invalid_code")));
}
})
.catch(() => { showToast("Error verifying TOTP code."); });
.catch(() => { showToast(t("error_verifying_totp_code")); });
});
// Focus the input and attach enter key listener
@@ -429,7 +431,7 @@ export function openTOTPModal() {
}
function loadTOTPQRCode() {
fetch("totp_setup.php", {
fetch("api/totp_setup.php", {
method: "GET",
credentials: "include",
headers: {
@@ -451,7 +453,7 @@ function loadTOTPQRCode() {
})
.catch(error => {
console.error("Error loading TOTP QR code:", error);
showToast("Error loading QR code.");
showToast(t("error_loading_qr_code"));
});
}
@@ -468,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: {
@@ -479,17 +481,88 @@ export function closeTOTPModal(disable = true) {
.then(r => r.json())
.then(result => {
if (!result.success) {
showToast("Error disabling TOTP setting: " + result.error);
showToast(t("error_disabling_totp_setting") + ": " + result.error);
}
})
.catch(() => { showToast("Error disabling TOTP setting."); });
.catch(() => { showToast(t("error_disabling_totp_setting")); });
}
}
// Global variable to hold the initial state of the admin form.
let originalAdminConfig = {};
// Capture the initial state of the admin form fields.
function captureInitialAdminConfig() {
originalAdminConfig = {
headerTitle: document.getElementById("headerTitle").value.trim(),
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
oidcClientId: document.getElementById("oidcClientId").value.trim(),
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
disableFormLogin: document.getElementById("disableFormLogin").checked,
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
};
}
// Compare current values to the captured initial state.
function hasUnsavedChanges() {
return (
document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle ||
document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl ||
document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId ||
document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret ||
document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri ||
document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin ||
document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth ||
document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin ||
document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl
);
}
// Use your custom confirmation modal.
function showCustomConfirmModal(message) {
return new Promise((resolve) => {
// Get modal elements from DOM.
const modal = document.getElementById("customConfirmModal");
const messageElem = document.getElementById("confirmMessage");
const yesBtn = document.getElementById("confirmYesBtn");
const noBtn = document.getElementById("confirmNoBtn");
// Set the message in the modal.
messageElem.textContent = message;
modal.style.display = "block";
// Define event handlers.
function onYes() {
cleanup();
resolve(true);
}
function onNo() {
cleanup();
resolve(false);
}
// Remove event listeners and hide modal after choice.
function cleanup() {
yesBtn.removeEventListener("click", onYes);
noBtn.removeEventListener("click", onNo);
modal.style.display = "none";
}
yesBtn.addEventListener("click", onYes);
noBtn.addEventListener("click", onNo);
});
}
export function openAdminPanel() {
fetch("getConfig.php", { credentials: "include" })
fetch("api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
if (config.header_title) {
document.querySelector(".header-title h1").textContent = config.header_title;
window.headerTitle = config.header_title || "FileRise";
}
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
const isDarkMode = document.body.classList.contains("dark-mode");
@@ -523,77 +596,84 @@ export function openAdminPanel() {
align-items: center;
z-index: 3000;
`;
// Added a version number next to "Admin Panel"
adminModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>
<h3>${adminTitle}</h3>
</h3>
<h3>${adminTitle}</h3>
<form id="adminPanelForm">
<fieldset style="margin-bottom: 15px;">
<legend>User Management</legend>
<legend>${t("user_management")}</legend>
<div style="display: flex; gap: 10px;">
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>OIDC Configuration</legend>
<legend>Header Settings</legend>
<div class="form-group">
<label for="oidcProviderUrl">OIDC Provider URL:</label>
<label for="headerTitle">Header Title:</label>
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>${t("login_options")}</legend>
<div class="form-group">
<input type="checkbox" id="disableFormLogin" />
<label for="disableFormLogin">${t("disable_login_form")}</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableBasicAuth" />
<label for="disableBasicAuth">${t("disable_basic_http_auth")}</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableOIDCLogin" />
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>${t("oidc_configuration")}</legend>
<div class="form-group">
<label for="oidcProviderUrl">${t("oidc_provider_url")}:</label>
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
</div>
<div class="form-group">
<label for="oidcClientId">OIDC Client ID:</label>
<label for="oidcClientId">${t("oidc_client_id")}:</label>
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
</div>
<div class="form-group">
<label for="oidcClientSecret">OIDC Client Secret:</label>
<label for="oidcClientSecret">${t("oidc_client_secret")}:</label>
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
</div>
<div class="form-group">
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
<label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label>
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Global TOTP Settings</legend>
<legend>${t("global_totp_settings")}</legend>
<div class="form-group">
<label for="globalOtpauthUrl">Global OTPAuth URL:</label>
<label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label>
<input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Login Options</legend>
<div class="form-group">
<input type="checkbox" id="disableFormLogin" />
<label for="disableFormLogin">Disable Login Form</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableBasicAuth" />
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableOIDCLogin" />
<label for="disableOIDCLogin">Disable OIDC Login</label>
</div>
</fieldset>
<div style="display: flex; justify-content: space-between;">
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
</div>
</form>
</div>
`;
document.body.appendChild(adminModal);
// Bind closing events that will use our enhanced close function.
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
adminModal.addEventListener("click", (e) => {
if (e.target === adminModal) closeAdminPanel();
});
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
// Bind other buttons.
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
toggleVisibility("addUserModal", true);
document.getElementById("newUsername").focus();
@@ -604,17 +684,17 @@ export function openAdminPanel() {
}
toggleVisibility("removeUserModal", true);
});
// New event binding for the User Permissions button:
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
openUserPermissionsModal();
});
document.getElementById("saveAdminSettings").addEventListener("click", () => {
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
if (totalDisabled === 3) {
showToast("At least one login method must remain enabled.");
showToast(t("at_least_one_login_method"));
disableOIDCLoginCheckbox.checked = false;
localStorage.setItem("disableOIDCLogin", "false");
if (typeof window.updateLoginOptionsUI === "function") {
@@ -626,6 +706,8 @@ 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(),
@@ -636,7 +718,8 @@ 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,
disableBasicAuth,
@@ -645,27 +728,32 @@ export function openAdminPanel() {
}, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast("Settings updated successfully.");
showToast(t("settings_updated_successfully"));
localStorage.setItem("disableFormLogin", disableFormLogin);
localStorage.setItem("disableBasicAuth", disableBasicAuth);
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
if (typeof window.updateLoginOptionsUI === "function") {
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
}
// Update the captured initial state since the changes have now been saved.
captureInitialAdminConfig();
closeAdminPanel();
loadAdminConfigFunc();
} else {
showToast("Error updating settings: " + (response.error || "Unknown error"));
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => { });
});
// Enforce login option constraints.
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
function enforceLoginOptionConstraint(changedCheckbox) {
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
if (changedCheckbox.checked && totalDisabled === 3) {
showToast("At least one login method must remain enabled.");
showToast(t("at_least_one_login_method"));
changedCheckbox.checked = false;
}
}
@@ -676,6 +764,9 @@ export function openAdminPanel() {
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
// Capture initial state after the modal loads.
captureInitialAdminConfig();
} else {
adminModal.style.backgroundColor = overlayBackground;
const modalContent = adminModal.querySelector(".modal-content");
@@ -693,6 +784,7 @@ export function openAdminPanel() {
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
adminModal.style.display = "flex";
captureInitialAdminConfig();
}
})
.catch(() => {
@@ -714,19 +806,25 @@ export function openAdminPanel() {
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
adminModal.style.display = "flex";
captureInitialAdminConfig();
} else {
openAdminPanel();
}
});
}
export function closeAdminPanel() {
export async function closeAdminPanel() {
if (hasUnsavedChanges()) {
const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm"));
if (!userConfirmed) {
return;
}
}
const adminModal = document.getElementById("adminPanelModal");
if (adminModal) adminModal.style.display = "none";
}
// --- New: User Permissions Modal ---
export function openUserPermissionsModal() {
let userPermissionsModal = document.getElementById("userPermissionsModal");
const isDarkMode = document.body.classList.contains("dark-mode");
@@ -759,13 +857,13 @@ export function openUserPermissionsModal() {
userPermissionsModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>User Permissions</h3>
<h3>${t("user_permissions")}</h3>
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
<!-- User rows will be loaded here -->
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">Cancel</button>
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">Save Permissions</button>
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
</div>
</div>
`;
@@ -793,17 +891,17 @@ 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("User permissions updated successfully.");
showToast(t("user_permissions_updated_successfully"));
userPermissionsModal.style.display = "none";
} else {
showToast("Error updating permissions: " + (response.error || "Unknown error"));
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => {
showToast("Error updating permissions.");
showToast(t("error_updating_permissions"));
});
});
} else {
@@ -819,23 +917,23 @@ 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 || []);
if (users.length === 0) {
listContainer.innerHTML = "<p>No users found.</p>";
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
return;
}
users.forEach(user => {
// Skip admin users.
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
// Use stored permissions if available; otherwise fall back to localStorage defaults.
// Use stored permissions if available; otherwise fall back to defaults.
const defaultPerm = {
folderOnly: false,
readOnly: false,
@@ -859,15 +957,15 @@ function loadUserPermissionsList() {
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
User Folder Only
${t("user_folder_only")}
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
Read Only
${t("read_only")}
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
Disable Upload
${t("disable_upload")}
</label>
</div>
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
@@ -877,6 +975,6 @@ function loadUserPermissionsList() {
});
})
.catch(() => {
listContainer.innerHTML = "<p>Error loading users.</p>";
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
});
}

View File

@@ -90,23 +90,36 @@ export function showToast(message, duration = 3000) {
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
const safeSearchTerm = escapeHTML(searchTerm);
// Choose the placeholder text based on advanced search mode
const placeholderText = window.advancedSearchEnabled
? t("search_placeholder_advanced")
: t("search_placeholder");
return `
<div class="row align-items-center mb-3">
<div class="col-12 col-md-8 mb-2 mb-md-0">
<div class="input-group">
<!-- Advanced Search Toggle Button -->
<div class="input-group-prepend">
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" onclick="toggleAdvancedSearch()" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
<i class="material-icons">${window.advancedSearchEnabled ? "filter_alt_off" : "filter_alt"}</i>
</button>
</div>
<!-- Search Icon -->
<div class="input-group-prepend">
<span class="input-group-text" id="searchIcon">
<i class="material-icons">search</i>
</span>
</div>
<input type="text" id="searchInput" class="form-control" placeholder="${t("search_placeholder")}" value="${safeSearchTerm}" aria-describedby="searchIcon">
<!-- Search Input -->
<input type="text" id="searchInput" class="form-control" placeholder="${placeholderText}" value="${safeSearchTerm}" aria-describedby="searchIcon">
</div>
</div>
<div class="col-12 col-md-4 text-left">
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">Prev</button>
<span class="page-indicator">Page ${currentPage} of ${totalPages || 1}</span>
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">Next</button>
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">${t("prev")}</button>
<span class="page-indicator">${t("page")} ${currentPage} ${t("of")} ${totalPages || 1}</span>
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">${t("next")}</button>
</div>
</div>
</div>
@@ -124,7 +137,7 @@ export function buildFileTableHeader(sortOrder) {
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">${t("file_size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>Actions</th>
<th>${t("actions")}</th>
</tr>
</thead>
`;
@@ -168,36 +181,38 @@ export function buildFileTableRow(file, folderPath) {
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
<button type="button" class="btn btn-sm btn-success download-btn"
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
title="Download">
<i class="material-icons">file_download</i>
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="Edit">
title="${t('edit')}">
<i class="material-icons">edit</i>
</button>
` : ""}
${previewButton}
<button class="btn btn-sm btn-warning rename-btn"
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
title="Rename">
title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
</div>
</td>
</tr>
`;
`;
}
export function buildBottomControls(itemsPerPageSetting) {
return `
<div class="d-flex align-items-center mt-3 bottom-controls">
<label class="label-inline mr-2 mb-0">Show</label>
<label class="label-inline mr-2 mb-0">${t("show")}</label>
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
${[10, 20, 50, 100].map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`).join("")}
${[10, 20, 50, 100]
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
.join("")}
</select>
<span class="items-per-page-text ml-2 mb-0">items per page</span>
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
</div>
`;
}

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

@@ -24,6 +24,9 @@ window.itemsPerPage = window.itemsPerPage || 10;
window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
// Global flag for advanced search mode.
window.advancedSearchEnabled = false;
/**
* --- Helper Functions ---
*/
@@ -70,24 +73,59 @@ function buildFolderSummary(filteredFiles) {
return sum + parseSizeToBytes(file.size);
}, 0);
const sizeStr = formatSize(totalBytes);
return `<strong>Total Files:</strong> ${totalFiles} &nbsp;|&nbsp; <strong>Total Size:</strong> ${sizeStr}`;
return `<strong>${t('total_files')}:</strong> ${totalFiles} &nbsp;|&nbsp; <strong>${t('total_size')}:</strong> ${sizeStr}`;
}
/**
* --- Advanced Search Toggle ---
* Toggles advanced search mode. When enabled, the search will include additional keys (e.g. "content").
*/
function toggleAdvancedSearch() {
window.advancedSearchEnabled = !window.advancedSearchEnabled;
const advancedBtn = document.getElementById("advancedSearchToggle");
if (advancedBtn) {
advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search";
}
// Re-run the file table rendering with updated search settings.
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.
* Searches over file name, uploader, and tag names.
* By default, searches over file name, uploader, and tag names.
* When advanced search is enabled, it also includes the 'content' property.
*/
function searchFiles(searchTerm) {
if (!searchTerm) return fileData;
// Define search options adjust threshold as needed.
// Define search keys.
let keys = [
{ 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 });
}
const options = {
keys: ['name', 'uploader', 'tags.name'],
threshold: 0.3
keys: keys,
threshold: 0.4,
minMatchCharLength: 2,
ignoreLocation: true
};
const fuse = new Fuse(fileData, options);
// Fuse returns an array of results where each result has an "item" property.
return fuse.search(searchTerm).map(result => result.item);
let results = fuse.search(searchTerm);
return results.map(result => result.item);
}
/**
@@ -98,19 +136,39 @@ export function createViewToggleButton() {
if (!toggleBtn) {
toggleBtn = document.createElement("button");
toggleBtn.id = "toggleViewBtn";
toggleBtn.classList.add("btn", "btn-secondary");
const titleElem = document.getElementById("fileListTitle");
if (titleElem) {
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
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.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
toggleBtn.onclick = () => {
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder);
toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("switch_to_gallery_view");
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;
}
@@ -135,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();
@@ -147,7 +205,7 @@ export function loadFileList(folderParam) {
.then(data => {
fileListContainer.innerHTML = ""; // Clear loading message.
if (data.files && Object.keys(data.files).length > 0) {
// In case the returned "files" is an object instead of an array, transform it.
// If the returned "files" is an object instead of an array, transform it.
if (!Array.isArray(data.files)) {
data.files = Object.entries(data.files).map(([name, meta]) => {
meta.name = name;
@@ -162,6 +220,8 @@ export function loadFileList(folderParam) {
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
file.type = "image";
}
// OPTIONAL: For text documents, preload content (if available from backend)
// Example: if (/\.txt|html|md|js|css|json|xml$/i.test(file.name)) { file.content = file.content || ""; }
return file;
});
fileData = data.files;
@@ -187,6 +247,7 @@ export function loadFileList(folderParam) {
// Render view based on the view mode.
if (window.viewMode === "gallery") {
renderGalleryView(folder);
updateFileActionButtons();
} else {
renderFileTable(folder);
}
@@ -223,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) {
@@ -234,11 +295,15 @@ export function renderFileTable(folder, container) {
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
// Build the top controls and append the advanced search toggle button.
const topControlsHTML = buildSearchAndPaginationControls({
currentPage,
totalPages,
searchTerm: window.currentSearchTerm || ""
});
const combinedTopHTML = topControlsHTML;
let headerHTML = buildFileTableHeader(sortOrder);
const startIndex = (currentPage - 1) * itemsPerPageSetting;
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
@@ -259,7 +324,7 @@ export function renderFileTable(folder, container) {
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
return p1 + p2 + tagBadgesHTML + p3;
});
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="${t('share')}">
<i class="material-icons">share</i>
</button>$1`);
rowsHTML += rowHTML;
@@ -270,7 +335,7 @@ export function renderFileTable(folder, container) {
rowsHTML += "</tbody></table>";
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
fileListContent.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
createViewToggleButton();
@@ -328,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;">`;
@@ -358,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="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="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="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="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;
@@ -492,4 +650,5 @@ window.changeItemsPerPage = function (newCount) {
window.loadFileList = loadFileList;
window.renderFileTable = renderFileTable;
window.renderGalleryView = renderGalleryView;
window.sortFiles = sortFiles;
window.sortFiles = sortFiles;
window.toggleAdvancedSearch = toggleAdvancedSearch;

View File

@@ -26,7 +26,7 @@ export function openShareModal(file, folder) {
<option value="240">240 minutes</option>
<option value="1440">1 Day</option>
</select>
<p>Password (optional):</p>
<p>${t("password_optional")}</p>
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
<br>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
@@ -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

@@ -28,7 +28,7 @@ export function openFolderShareModal(folder) {
<option value="1440">1 ${t("day")}</option>
</select>
<p>${t("password_optional")}</p>
<input type="text" id="folderSharePassword" placeholder="${t("password")}" style="width: 100%;"/>
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
<br>
<label>
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
@@ -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: {

966
public/js/i18n.js Normal file
View File

@@ -0,0 +1,966 @@
/* i18n.js */
const translations = {
en: {
"please_log_in_to_continue": "Please log in to continue.",
"no_files_selected": "No files selected.",
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
"element_not_found": "Element with id \"{id}\" not found.",
"search_placeholder": "Search files, tags, & uploader...",
"search_placeholder_advanced": "Advanced Search: files, tags, uploader & content...",
"basic_search_tooltip": "Basic Search: Search by file name, tags, and uploader.",
"advanced_search_tooltip": "Advanced Search: Includes file content, in addition to file name, tags, and uploader.",
"file_name": "File Name",
"date_modified": "Date Modified",
"upload_date": "Upload Date",
"file_size": "File Size",
"uploader": "Uploader",
"enter_totp_code": "Enter TOTP Code",
"use_recovery_code_instead": "Use Recovery Code instead",
"enter_recovery_code": "Enter Recovery Code",
"editing": "Editing",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Save",
"close": "Close",
"no_files_found": "No files found.",
"switch_to_table_view": "Switch to Table View",
"switch_to_gallery_view": "Switch to Gallery View",
"share_file": "Share File",
"set_expiration": "Set Expiration:",
"password_optional": "Password (optional):",
"generate_share_link": "Generate Share Link",
"shareable_link": "Shareable Link:",
"copy_link": "Copy Link",
"tag_file": "Tag File",
"tag_name": "Tag Name:",
"tag_color": "Tag Color:",
"save_tag": "Save Tag",
"light_mode": "Light Mode",
"dark_mode": "Dark Mode",
"upload_instruction": "Drop files/folders here or click 'Choose files'",
"no_files_selected_default": "No files selected",
"choose_files": "Choose files",
"delete_selected": "Delete Selected",
"copy_selected": "Copy Selected",
"move_selected": "Move Selected",
"tag_selected": "Tag Selected",
"download_zip": "Download Zip",
"extract_zip": "Extract Zip",
"preview": "Preview",
"edit": "Edit",
"rename": "Rename",
"trash_empty": "Trash is empty.",
"no_trash_selected": "No trash items selected for restore.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Logout",
"change_password": "Change Password",
"restore_text": "Restore or",
"delete_text": "Delete Trash Items",
"restore_selected": "Restore Selected",
"restore_all": "Restore All",
"delete_selected_trash": "Delete Selected",
"delete_all": "Delete All",
"upload_header": "Upload Files/Folders",
// Folder Management keys:
"folder_navigation": "Folder Navigation & Management",
"create_folder": "Create Folder",
"create_folder_title": "Create Folder",
"enter_folder_name": "Enter folder name",
"cancel": "Cancel",
"create": "Create",
"rename_folder": "Rename Folder",
"rename_folder_title": "Rename Folder",
"rename_folder_placeholder": "Enter new folder name",
"delete_folder": "Delete Folder",
"delete_folder_title": "Delete Folder",
"delete_folder_message": "Are you sure you want to delete this folder?",
"folder_help": "Folder Help",
"folder_help_item_1": "Click on a folder in the tree to view its files.",
"folder_help_item_2": "Use [-] to collapse and [+] to expand folders.",
"folder_help_item_3": "Select a folder and click \"Create Folder\" to add a subfolder.",
"folder_help_item_4": "To rename or delete a folder, select it and then click the appropriate button.",
// File List keys:
"actions": "Actions",
"file_list_title": "Files in (Root)",
"files_in": "Files in",
"delete_files": "Delete Files",
"delete_selected_files_title": "Delete Selected Files",
"delete_files_message": "Are you sure you want to delete the selected files?",
"copy_files": "Copy Files",
"copy_files_title": "Copy Selected Files",
"copy_files_message": "Select a target folder for copying the selected files:",
"move_files": "Move Files",
"move_files_title": "Move Selected Files",
"move_files_message": "Select a target folder for moving the selected files:",
"move": "Move",
"extract_zip_button": "Extract Zip",
"download_zip_title": "Download Selected Files as Zip",
"download_zip_prompt": "Enter a name for the zip file:",
"zip_placeholder": "files.zip",
"share": "Share",
"total_files": "Total Files",
"total_size": "Total Size",
"prev": "Prev",
"next": "Next",
"page": "Page",
"of": "of",
// Login Form keys:
"login": "Login",
"remember_me": "Remember me",
"login_oidc": "Login with OIDC",
"basic_http_login": "Use Basic HTTP Login",
// Change Password keys:
"change_password_title": "Change Password",
"old_password": "Old Password",
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
// Add User keys:
"create_new_user_title": "Create New User",
"username": "Username:",
"password": "Password:",
"enter_password": "Password",
"preparing_download": "Preparing your download...",
"download_file": "Download File",
"confirm_or_change_filename": "Confirm or change the download file name:",
"filename": "Filename",
"cancel": "Cancel",
"download": "Download",
"grant_admin": "Grant Admin Access",
"save_user": "Save User",
// Remove User keys:
"remove_user_title": "Remove User",
"select_user_remove": "Select a user to remove:",
"delete_user": "Delete User",
// Rename File keys:
"rename_file_title": "Rename File",
"rename_file_placeholder": "Enter new file name",
// Folder Share
"share_folder": "Share Folder",
"allow_uploads": "Allow Uploads",
"share_link_generated": "Share Link Generated",
"error_generating_share_link": "Error Generating Share Link",
// Folder
"folder_share": "Share Folder",
// Custom Confirm Modal keys:
"yes": "Yes",
"no": "No",
"unsaved_changes_confirm": "You have unsaved changes. Are you sure you want to close without saving?",
"delete": "Delete",
"download": "Download",
"upload": "Upload",
"copy": "Copy",
"extract": "Extract",
"user": "User:",
"unknown_error": "Unknown Error",
"link_copied": "Link Copied to Clipboard",
"minutes": "minutes",
"hours": "hours",
"days": "days",
"weeks": "weeks",
"months": "months",
"seconds": "seconds",
// Dark Mode Toggle
"dark_mode_toggle": "Dark Mode",
"light_mode_toggle": "Light Mode",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Admin Panel",
"user_panel": "User Panel",
"trash_restore_delete": "Trash Restore/Delete",
"totp_settings": "TOTP Settings",
"enable_totp": "Enable TOTP",
"language": "Language",
"select_language": "Select Language",
"english": "English",
"spanish": "Spanish",
"french": "French",
"german": "German",
"use_totp_code_instead": "Use TOTP Code instead",
"submit_recovery_code": "Submit Recovery Code",
"please_enter_recovery_code": "Please enter your recovery code.",
"recovery_code_verification_failed": "Recovery code verification failed",
"error_verifying_recovery_code": "Error verifying recovery code",
"totp_verification_failed": "TOTP verification failed",
"error_verifying_totp_code": "Error verifying TOTP code",
"totp_setup": "TOTP Setup",
"scan_qr_code": "Scan this QR code with your authenticator app.",
"enter_totp_confirmation": "Enter the 6-digit code from your app to confirm setup:",
"confirm": "Confirm",
"please_enter_valid_code": "Please enter a valid 6-digit code.",
"totp_enabled_successfully": "TOTP successfully enabled.",
"error_generating_recovery_code": "Error generating recovery code",
"error_loading_qr_code": "Error loading QR code.",
"error_disabling_totp_setting": "Error disabling TOTP setting",
"user_management": "User Management",
"add_user": "Add User",
"remove_user": "Remove User",
"user_permissions": "User Permissions",
"oidc_configuration": "OIDC Configuration",
"oidc_provider_url": "OIDC Provider URL",
"oidc_client_id": "OIDC Client ID",
"oidc_client_secret": "OIDC Client Secret",
"oidc_redirect_uri": "OIDC Redirect URI",
"global_totp_settings": "Global TOTP Settings",
"global_otpauth_url": "Global OTPAuth URL",
"login_options": "Login Options",
"disable_login_form": "Disable Login Form",
"disable_basic_http_auth": "Disable Basic HTTP Auth",
"disable_oidc_login": "Disable OIDC Login",
"save_settings": "Save Settings",
"at_least_one_login_method": "At least one login method must remain enabled.",
"settings_updated_successfully": "Settings updated successfully.",
"error_updating_settings": "Error updating settings",
"user_permissions_updated_successfully": "User permissions updated successfully.",
"error_updating_permissions": "Error updating permissions",
"no_users_found": "No users found.",
"user_folder_only": "User Folder Only",
"read_only": "Read Only",
"disable_upload": "Disable Upload",
"error_loading_users": "Error loading users",
"save_permissions": "Save Permissions",
"your_recovery_code": "Your Recovery Code",
"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",
"columns":"Columns"
},
es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
"no_files_selected": "No se han seleccionado archivos.",
"confirm_delete_files": "¿Está seguro de que desea eliminar {count} archivo(s) seleccionado(s)?",
"element_not_found": "Elemento con id \"{id}\" no encontrado.",
"search_placeholder": "Buscar archivos, etiquetas y cargador...",
"search_placeholder_advanced": "Búsqueda avanzada: archivos, etiquetas, cargador y contenido...",
"basic_search_tooltip": "Búsqueda básica: Buscar por nombre de archivo, etiquetas y cargador.",
"advanced_search_tooltip": "Búsqueda avanzada: Incluye el contenido del archivo, además del nombre, etiquetas y cargador.",
"file_name": "Nombre del archivo",
"date_modified": "Fecha de modificación",
"upload_date": "Fecha de carga",
"file_size": "Tamaño del archivo",
"uploader": "Cargado por",
"enter_totp_code": "Ingrese el código TOTP",
"use_recovery_code_instead": "Usar código de recuperación en su lugar",
"enter_recovery_code": "Ingrese el código de recuperación",
"editing": "Editando",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Guardar",
"close": "Cerrar",
"no_files_found": "No se encontraron archivos.",
"switch_to_table_view": "Cambiar a vista de tabla",
"switch_to_gallery_view": "Cambiar a vista de galería",
"share_file": "Compartir archivo",
"set_expiration": "Establecer vencimiento:",
"password_optional": "Contraseña (opcional):",
"generate_share_link": "Generar enlace para compartir",
"shareable_link": "Enlace para compartir:",
"copy_link": "Copiar enlace",
"tag_file": "Etiquetar archivo",
"tag_name": "Nombre de la etiqueta:",
"tag_color": "Color de la etiqueta:",
"save_tag": "Guardar etiqueta",
"light_mode": "Modo claro",
"dark_mode": "Modo oscuro",
"upload_instruction": "Suelte archivos/carpetas aquí o haga clic en 'Elegir archivos'",
"no_files_selected_default": "No se han seleccionado archivos",
"choose_files": "Elegir archivos",
"delete_selected": "Eliminar seleccionados",
"copy_selected": "Copiar seleccionados",
"move_selected": "Mover seleccionados",
"tag_selected": "Etiquetar seleccionados",
"download_zip": "Descargar Zip",
"extract_zip": "Extraer Zip",
"preview": "Vista previa",
"edit": "Editar",
"rename": "Renombrar",
"trash_empty": "La papelera está vacía.",
"no_trash_selected": "No se han seleccionado elementos de la papelera para restaurar.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Cerrar sesión",
"change_password": "Cambiar contraseña",
"restore_text": "Restaurar o",
"delete_text": "Eliminar elementos de la papelera",
"restore_selected": "Restaurar seleccionados",
"restore_all": "Restaurar todo",
"delete_selected_trash": "Eliminar seleccionados",
"delete_all": "Eliminar todo",
"upload_header": "Cargar archivos/carpetas",
// Folder Management keys:
"folder_navigation": "Navegación y gestión de carpetas",
"create_folder": "Crear carpeta",
"create_folder_title": "Crear carpeta",
"enter_folder_name": "Ingrese el nombre de la carpeta",
"cancel": "Cancelar",
"create": "Crear",
"rename_folder": "Renombrar carpeta",
"rename_folder_title": "Renombrar carpeta",
"rename_folder_placeholder": "Ingrese el nuevo nombre de la carpeta",
"delete_folder": "Eliminar carpeta",
"delete_folder_title": "Eliminar carpeta",
"delete_folder_message": "¿Está seguro de que desea eliminar esta carpeta?",
"folder_help": "Ayuda de carpetas",
"folder_help_item_1": "Haga clic en una carpeta en el árbol para ver sus archivos.",
"folder_help_item_2": "Utilice [-] para contraer y [+] para expandir las carpetas.",
"folder_help_item_3": "Seleccione una carpeta y haga clic en \"Crear carpeta\" para agregar una subcarpeta.",
"folder_help_item_4": "Para renombrar o eliminar una carpeta, selecciónela y luego haga clic en el botón correspondiente.",
// File List keys:
"file_list_title": "Archivos en (Raíz)",
"files_in": "Archivos en",
"delete_files": "Eliminar archivos",
"delete_selected_files_title": "Eliminar archivos seleccionados",
"delete_files_message": "¿Está seguro de que desea eliminar los archivos seleccionados?",
"copy_files": "Copiar archivos",
"copy_files_title": "Copiar archivos seleccionados",
"copy_files_message": "Seleccione una carpeta destino para copiar los archivos seleccionados:",
"move_files": "Mover archivos",
"move_files_title": "Mover archivos seleccionados",
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
"move": "Mover",
"extract_zip_button": "Extraer Zip",
"download_zip_title": "Descargar archivos seleccionados en un Zip",
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Iniciar sesión",
"remember_me": "Recuérdame",
"login_oidc": "Iniciar sesión con OIDC",
"basic_http_login": "Usar autenticación HTTP básica",
// Change Password keys:
"change_password_title": "Cambiar contraseña",
"old_password": "Contraseña antigua",
"new_password": "Nueva contraseña",
"confirm_new_password": "Confirmar nueva contraseña",
// Add User keys:
"create_new_user_title": "Crear nuevo usuario",
"username": "Usuario:",
"password": "Contraseña:",
"enter_password": "Contraseña",
"preparing_download": "Preparando su descarga...",
"download_file": "Descargar Archivo",
"confirm_or_change_filename": "Confirme o cambie el nombre del archivo a descargar:",
"filename": "Nombre de archivo",
"cancel": "Cancelar",
"download": "Descargar",
"grant_admin": "Otorgar acceso de administrador",
"save_user": "Guardar usuario",
// Remove User keys:
"remove_user_title": "Eliminar usuario",
"select_user_remove": "Seleccione un usuario para eliminar:",
"delete_user": "Eliminar usuario",
// Rename File keys:
"rename_file_title": "Renombrar archivo",
"rename_file_placeholder": "Ingrese el nuevo nombre del archivo",
// Folder Share
"share_folder": "Compartir carpeta",
"allow_uploads": "Permitir cargas",
"share_link_generated": "Enlace para compartir generado",
"error_generating_share_link": "Error al generar el enlace para compartir",
// Folder
"folder_share": "Compartir carpeta",
// Custom Confirm Modal keys:
"yes": "Sí",
"no": "No",
"unsaved_changes_confirm": "Tiene cambios sin guardar. ¿Está seguro de que desea cerrar sin guardar?",
"delete": "Eliminar",
"download": "Descargar",
"upload": "Cargar",
"copy": "Copiar",
"extract": "Extraer",
"user": "Usuario:",
"unknown_error": "Error desconocido",
"link_copied": "Enlace copiado al portapapeles",
"minutes": "minutos",
"hours": "horas",
"days": "días",
"weeks": "semanas",
"months": "meses",
"seconds": "segundos",
// Dark Mode Toggle
"dark_mode_toggle": "Modo oscuro",
"light_mode_toggle": "Modo claro",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Panel de Administración",
"user_panel": "Panel de Usuario",
"totp_settings": "Configuración TOTP",
"enable_totp": "Activar TOTP",
"language": "Idioma",
"select_language": "Seleccionar idioma",
"english": "Inglés",
"spanish": "Español",
"french": "Francés",
"german": "Alemán",
"use_totp_code_instead": "Usar código TOTP en su lugar",
"submit_recovery_code": "Enviar código de recuperación",
"please_enter_recovery_code": "Por favor, ingrese su código de recuperación.",
"recovery_code_verification_failed": "La verificación del código de recuperación falló",
"error_verifying_recovery_code": "Error al verificar el código de recuperación",
"totp_verification_failed": "La verificación TOTP falló",
"error_verifying_totp_code": "Error al verificar el código TOTP",
"totp_setup": "Configuración TOTP",
"scan_qr_code": "Escanee este código QR con su aplicación de autenticación.",
"enter_totp_confirmation": "Ingrese el código de 6 dígitos de su aplicación para confirmar la configuración:",
"confirm": "Confirmar",
"please_enter_valid_code": "Por favor, ingrese un código válido de 6 dígitos.",
"totp_enabled_successfully": "TOTP activado con éxito.",
"error_generating_recovery_code": "Error al generar el código de recuperación",
"error_loading_qr_code": "Error al cargar el código QR.",
"error_disabling_totp_setting": "Error al desactivar la configuración TOTP",
"user_management": "Gestión de Usuarios",
"add_user": "Agregar usuario",
"remove_user": "Eliminar usuario",
"user_permissions": "Permisos de Usuario",
"oidc_configuration": "Configuración OIDC",
"oidc_provider_url": "URL del Proveedor OIDC",
"oidc_client_id": "ID del Cliente OIDC",
"oidc_client_secret": "Secreto del Cliente OIDC",
"oidc_redirect_uri": "URI de Redirección OIDC",
"global_totp_settings": "Configuración Global TOTP",
"global_otpauth_url": "URL Global OTPAuth",
"login_options": "Opciones de inicio de sesión",
"disable_login_form": "Desactivar formulario de inicio de sesión",
"disable_basic_http_auth": "Desactivar autenticación HTTP básica",
"disable_oidc_login": "Desactivar inicio de sesión OIDC",
"save_settings": "Guardar configuración",
"at_least_one_login_method": "Al menos un método de inicio de sesión debe permanecer habilitado.",
"settings_updated_successfully": "Configuración actualizada con éxito.",
"error_updating_settings": "Error al actualizar la configuración",
"user_permissions_updated_successfully": "Permisos de usuario actualizados con éxito.",
"error_updating_permissions": "Error al actualizar los permisos",
"no_users_found": "No se encontraron usuarios.",
"user_folder_only": "Solo carpeta de usuario",
"read_only": "Solo lectura",
"disable_upload": "Desactivar carga",
"error_loading_users": "Error al cargar usuarios",
"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",
"columns": "Columnas"
},
fr: {
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
"no_files_selected": "Aucun fichier sélectionné.",
"confirm_delete_files": "Êtes-vous sûr de vouloir supprimer {count} fichier(s) sélectionné(s) ?",
"element_not_found": "Élément avec l'id \"{id}\" non trouvé.",
"search_placeholder": "Rechercher des fichiers, des balises et l'uploader...",
"search_placeholder_advanced": "Recherche avancée : fichiers, balises, uploader et contenu...",
"basic_search_tooltip": "Recherche basique : rechercher par nom de fichier, balises et uploader.",
"advanced_search_tooltip": "Recherche avancée : inclut le contenu du fichier, en plus du nom, des balises et de l'uploader.",
"file_name": "Nom du fichier",
"date_modified": "Date de modification",
"upload_date": "Date de téléchargement",
"file_size": "Taille du fichier",
"uploader": "Uploader",
"enter_totp_code": "Entrez le code TOTP",
"use_recovery_code_instead": "Utilisez le code de récupération à la place",
"enter_recovery_code": "Entrez le code de récupération",
"editing": "Modification",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Enregistrer",
"close": "Fermer",
"no_files_found": "Aucun fichier trouvé.",
"switch_to_table_view": "Passer en vue tableau",
"switch_to_gallery_view": "Passer en vue galerie",
"share_file": "Partager le fichier",
"set_expiration": "Définir l'expiration :",
"password_optional": "Mot de passe (facultatif) :",
"generate_share_link": "Générer le lien de partage",
"shareable_link": "Lien partageable :",
"copy_link": "Copier le lien",
"tag_file": "Étiqueter le fichier",
"tag_name": "Nom de l'étiquette :",
"tag_color": "Couleur de l'étiquette :",
"save_tag": "Enregistrer l'étiquette",
"light_mode": "Mode clair",
"dark_mode": "Mode sombre",
"upload_instruction": "Déposez des fichiers/dossiers ici ou cliquez sur 'Choisir des fichiers'",
"no_files_selected_default": "Aucun fichier sélectionné",
"choose_files": "Choisir des fichiers",
"delete_selected": "Supprimer la sélection",
"copy_selected": "Copier la sélection",
"move_selected": "Déplacer la sélection",
"tag_selected": "Étiqueter la sélection",
"download_zip": "Télécharger le Zip",
"extract_zip": "Extraire le Zip",
"preview": "Aperçu",
"edit": "Modifier",
"rename": "Renommer",
"trash_empty": "La corbeille est vide.",
"no_trash_selected": "Aucun élément de la corbeille sélectionné pour restauration.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Déconnexion",
"change_password": "Changer le mot de passe",
"restore_text": "Restaurer ou",
"delete_text": "Supprimer les éléments de la corbeille",
"restore_selected": "Restaurer la sélection",
"restore_all": "Restaurer tout",
"delete_selected_trash": "Supprimer la sélection",
"delete_all": "Supprimer tout",
"upload_header": "Téléverser des fichiers/dossiers",
// Folder Management keys:
"folder_navigation": "Navigation et gestion des dossiers",
"create_folder": "Créer un dossier",
"create_folder_title": "Créer un dossier",
"enter_folder_name": "Entrez le nom du dossier",
"cancel": "Annuler",
"create": "Créer",
"rename_folder": "Renommer le dossier",
"rename_folder_title": "Renommer le dossier",
"rename_folder_placeholder": "Entrez le nouveau nom du dossier",
"delete_folder": "Supprimer le dossier",
"delete_folder_title": "Supprimer le dossier",
"delete_folder_message": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
"folder_help": "Aide des dossiers",
"folder_help_item_1": "Cliquez sur un dossier dans l'arborescence pour voir ses fichiers.",
"folder_help_item_2": "Utilisez [-] pour réduire et [+] pour développer les dossiers.",
"folder_help_item_3": "Sélectionnez un dossier et cliquez sur \"Créer un dossier\" pour ajouter un sous-dossier.",
"folder_help_item_4": "Pour renommer ou supprimer un dossier, sélectionnez-le puis cliquez sur le bouton approprié.",
// File List keys:
"file_list_title": "Fichiers dans (Racine)",
"files_in": "Fichiers dans",
"delete_files": "Supprimer les fichiers",
"delete_selected_files_title": "Supprimer les fichiers sélectionnés",
"delete_files_message": "Êtes-vous sûr de vouloir supprimer les fichiers sélectionnés ?",
"copy_files": "Copier les fichiers",
"copy_files_title": "Copier les fichiers sélectionnés",
"copy_files_message": "Sélectionnez un dossier de destination pour copier les fichiers sélectionnés :",
"move_files": "Déplacer les fichiers",
"move_files_title": "Déplacer les fichiers sélectionnés",
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
"move": "Déplacer",
"extract_zip_button": "Extraire le Zip",
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Connexion",
"remember_me": "Se souvenir de moi",
"login_oidc": "Se connecter avec OIDC",
"basic_http_login": "Utiliser l'authentification HTTP basique",
// Change Password keys:
"change_password_title": "Changer le mot de passe",
"old_password": "Ancien mot de passe",
"new_password": "Nouveau mot de passe",
"confirm_new_password": "Confirmer le nouveau mot de passe",
// Add User keys:
"create_new_user_title": "Créer un nouvel utilisateur",
"username": "Nom d'utilisateur :",
"password": "Mot de passe :",
"enter_password": "Mot de passe",
"preparing_download": "Préparation de votre téléchargement...",
"download_file": "Télécharger le fichier",
"confirm_or_change_filename": "Confirmez ou modifiez le nom du fichier à télécharger :",
"filename": "Nom du fichier",
"cancel": "Annuler",
"download": "Télécharger",
"grant_admin": "Accorder l'accès administrateur",
"save_user": "Enregistrer l'utilisateur",
// Remove User keys:
"remove_user_title": "Supprimer un utilisateur",
"select_user_remove": "Sélectionnez un utilisateur à supprimer :",
"delete_user": "Supprimer l'utilisateur",
// Rename File keys:
"rename_file_title": "Renommer le fichier",
"rename_file_placeholder": "Entrez le nouveau nom du fichier",
// Folder Share
"share_folder": "Partager le dossier",
"allow_uploads": "Autoriser les téléchargements",
"share_link_generated": "Lien de partage généré",
"error_generating_share_link": "Erreur lors de la génération du lien de partage",
// Folder
"folder_share": "Partager le dossier",
// Custom Confirm Modal keys:
"yes": "Oui",
"no": "Non",
"unsaved_changes_confirm": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir fermer sans enregistrer ?",
"delete": "Supprimer",
"download": "Télécharger",
"upload": "Téléverser",
"copy": "Copier",
"extract": "Extraire",
"user": "Utilisateur :",
"unknown_error": "Erreur inconnue",
"link_copied": "Lien copié dans le presse-papiers",
"minutes": "minutes",
"hours": "heures",
"days": "jours",
"weeks": "semaines",
"months": "mois",
"seconds": "secondes",
// Dark Mode Toggle
"dark_mode_toggle": "Mode sombre",
"light_mode_toggle": "Mode clair",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Panneau d'administration",
"user_panel": "Panneau utilisateur",
"totp_settings": "Paramètres TOTP",
"enable_totp": "Activer TOTP",
"language": "Langue",
"select_language": "Sélectionnez la langue",
"english": "Anglais",
"spanish": "Espagnol",
"french": "Français",
"german": "Allemand",
"use_totp_code_instead": "Utiliser le code TOTP à la place",
"submit_recovery_code": "Soumettre le code de récupération",
"please_enter_recovery_code": "Veuillez entrer votre code de récupération.",
"recovery_code_verification_failed": "La vérification du code de récupération a échoué",
"error_verifying_recovery_code": "Erreur lors de la vérification du code de récupération",
"totp_verification_failed": "La vérification TOTP a échoué",
"error_verifying_totp_code": "Erreur lors de la vérification du code TOTP",
"totp_setup": "Configuration TOTP",
"scan_qr_code": "Scannez ce QR code avec votre application d'authentification.",
"enter_totp_confirmation": "Entrez le code à 6 chiffres de votre application pour confirmer la configuration :",
"confirm": "Confirmer",
"please_enter_valid_code": "Veuillez entrer un code valide à 6 chiffres.",
"totp_enabled_successfully": "TOTP activé avec succès.",
"error_generating_recovery_code": "Erreur lors de la génération du code de récupération",
"error_loading_qr_code": "Erreur lors du chargement du QR code.",
"error_disabling_totp_setting": "Erreur lors de la désactivation des paramètres TOTP",
"user_management": "Gestion des utilisateurs",
"add_user": "Ajouter un utilisateur",
"remove_user": "Supprimer un utilisateur",
"user_permissions": "Permissions des utilisateurs",
"oidc_configuration": "Configuration OIDC",
"oidc_provider_url": "URL du fournisseur OIDC",
"oidc_client_id": "ID du client OIDC",
"oidc_client_secret": "Secret du client OIDC",
"oidc_redirect_uri": "URI de redirection OIDC",
"global_totp_settings": "Paramètres globaux TOTP",
"global_otpauth_url": "URL globale OTPAuth",
"login_options": "Options de connexion",
"disable_login_form": "Désactiver le formulaire de connexion",
"disable_basic_http_auth": "Désactiver l'authentification HTTP basique",
"disable_oidc_login": "Désactiver la connexion OIDC",
"save_settings": "Enregistrer les paramètres",
"at_least_one_login_method": "Au moins une méthode de connexion doit rester activée.",
"settings_updated_successfully": "Paramètres mis à jour avec succès.",
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"user_permissions_updated_successfully": "Permissions des utilisateurs mises à jour avec succès.",
"error_updating_permissions": "Erreur lors de la mise à jour des permissions",
"no_users_found": "Aucun utilisateur trouvé.",
"user_folder_only": "Uniquement le dossier utilisateur",
"read_only": "Lecture seule",
"disable_upload": "Désactiver le téléchargement",
"error_loading_users": "Erreur lors du chargement des utilisateurs",
"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",
"columns": "Colonnes"
},
de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
"no_files_selected": "Keine Dateien ausgewählt.",
"confirm_delete_files": "Sind Sie sicher, dass Sie {count} ausgewählte Datei(en) löschen möchten?",
"element_not_found": "Element mit der ID \"{id}\" wurde nicht gefunden.",
"search_placeholder": "Dateien, Tags und Uploader suchen...",
"search_placeholder_advanced": "Erweiterte Suche: Dateien, Tags, Uploader & Inhalt...",
"basic_search_tooltip": "Einfache Suche: Nach Dateiname, Tags und Uploader suchen.",
"advanced_search_tooltip": "Erweiterte Suche: Beinhaltet Dateiinhalte zusätzlich zum Dateinamen, Tags und Uploader.",
"file_name": "Dateiname",
"date_modified": "Änderungsdatum",
"upload_date": "Hochladedatum",
"file_size": "Dateigröße",
"uploader": "Uploader",
"enter_totp_code": "Geben Sie den TOTP-Code ein",
"use_recovery_code_instead": "Verwenden Sie stattdessen den Wiederherstellungscode",
"enter_recovery_code": "Geben Sie den Wiederherstellungscode ein",
"editing": "Bearbeitung",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Speichern",
"close": "Schließen",
"no_files_found": "Keine Dateien gefunden.",
"switch_to_table_view": "Zur Tabellenansicht wechseln",
"switch_to_gallery_view": "Zur Galerieansicht wechseln",
"share_file": "Datei teilen",
"set_expiration": "Ablauf festlegen:",
"password_optional": "Passwort (optional):",
"generate_share_link": "Freigabelink generieren",
"shareable_link": "Freigabelink:",
"copy_link": "Link kopieren",
"tag_file": "Datei taggen",
"tag_name": "Tagname:",
"tag_color": "Tagfarbe:",
"save_tag": "Tag speichern",
"light_mode": "Heller Modus",
"dark_mode": "Dunkler Modus",
"upload_instruction": "Ziehen Sie Dateien/Ordner hierher oder klicken Sie auf 'Dateien auswählen'",
"no_files_selected_default": "Keine Dateien ausgewählt",
"choose_files": "Dateien auswählen",
"delete_selected": "Ausgewählte löschen",
"copy_selected": "Ausgewählte kopieren",
"move_selected": "Ausgewählte verschieben",
"tag_selected": "Ausgewählte taggen",
"download_zip": "Zip herunterladen",
"extract_zip": "Zip entpacken",
"preview": "Vorschau",
"edit": "Bearbeiten",
"rename": "Umbenennen",
"trash_empty": "Papierkorb ist leer.",
"no_trash_selected": "Keine Papierkorbeinträge zur Wiederherstellung ausgewählt.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Abmelden",
"change_password": "Passwort ändern",
"restore_text": "Wiederherstellen oder",
"delete_text": "Papierkorbeinträge löschen",
"restore_selected": "Ausgewählte wiederherstellen",
"restore_all": "Alle wiederherstellen",
"delete_selected_trash": "Ausgewählte löschen",
"delete_all": "Alle löschen",
"upload_header": "Dateien/Ordner hochladen",
// Folder Management keys:
"folder_navigation": "Ordnernavigation & Verwaltung",
"create_folder": "Ordner erstellen",
"create_folder_title": "Ordner erstellen",
"enter_folder_name": "Geben Sie den Ordnernamen ein",
"cancel": "Abbrechen",
"create": "Erstellen",
"rename_folder": "Ordner umbenennen",
"rename_folder_title": "Ordner umbenennen",
"rename_folder_placeholder": "Neuen Ordnernamen eingeben",
"delete_folder": "Ordner löschen",
"delete_folder_title": "Ordner löschen",
"delete_folder_message": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
"folder_help": "Ordnerhilfe",
"folder_help_item_1": "Klicken Sie auf einen Ordner im Baum, um dessen Dateien anzuzeigen.",
"folder_help_item_2": "Verwenden Sie [-] zum Einklappen und [+] zum Ausklappen der Ordner.",
"folder_help_item_3": "Wählen Sie einen Ordner aus und klicken Sie auf \"Ordner erstellen\", um einen Unterordner hinzuzufügen.",
"folder_help_item_4": "Um einen Ordner umzubenennen oder zu löschen, wählen Sie ihn aus und klicken Sie auf den entsprechenden Button.",
// File List keys:
"actions": "Aktionen",
"file_list_title": "Dateien in (Root)",
"files_in": "Dateien in",
"delete_files": "Dateien löschen",
"delete_selected_files_title": "Ausgewählte Dateien löschen",
"delete_files_message": "Sind Sie sicher, dass Sie die ausgewählten Dateien löschen möchten?",
"copy_files": "Dateien kopieren",
"copy_files_title": "Ausgewählte Dateien kopieren",
"copy_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu kopieren:",
"move_files": "Dateien verschieben",
"move_files_title": "Ausgewählte Dateien verschieben",
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
"move": "Verschieben",
"extract_zip_button": "Zip entpacken",
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
"zip_placeholder": "files.zip",
"share": "Teilen",
"total_files": "Gesamtanzahl",
"total_size": "Gesamtgröße",
"prev": "Zurück",
"next": "Weiter",
"page": "Seite",
"of": "von",
// Login Form keys:
"login": "Anmelden",
"remember_me": "Angemeldet bleiben",
"login_oidc": "Mit OIDC anmelden",
"basic_http_login": "HTTP-Basisauthentifizierung verwenden",
// Change Password keys:
"change_password_title": "Passwort ändern",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort",
"confirm_new_password": "Neues Passwort bestätigen",
// Add User keys:
"create_new_user_title": "Neuen Benutzer erstellen",
"username": "Benutzername:",
"password": "Passwort:",
"enter_password": "Passwort",
"preparing_download": "Bereite Ihren Download vor...",
"download_file": "Datei herunterladen",
"confirm_or_change_filename": "Bestätigen oder ändern Sie den Dateinamen zum Download:",
"filename": "Dateiname",
"cancel": "Abbrechen",
"download": "Herunterladen",
"grant_admin": "Admin-Rechte vergeben",
"save_user": "Benutzer speichern",
// Remove User keys:
"remove_user_title": "Benutzer entfernen",
"select_user_remove": "Wählen Sie einen Benutzer zum Entfernen aus:",
"delete_user": "Benutzer löschen",
// Rename File keys:
"rename_file_title": "Datei umbenennen",
"rename_file_placeholder": "Geben Sie den neuen Dateinamen ein",
// Folder Share
"share_folder": "Ordner teilen",
"allow_uploads": "Uploads erlauben",
"share_link_generated": "Freigabelink generiert",
"error_generating_share_link": "Fehler beim Generieren des Freigabelinks",
// Folder
"folder_share": "Ordner teilen",
// Custom Confirm Modal keys:
"yes": "Ja",
"no": "Nein",
"unsaved_changes_confirm": "Sie haben ungespeicherte Änderungen. Sind Sie sicher, dass Sie schließen möchten, ohne zu speichern?",
"delete": "Löschen",
"download": "Herunterladen",
"upload": "Hochladen",
"copy": "Kopieren",
"extract": "Entpacken",
"user": "Benutzer:",
"unknown_error": "Unbekannter Fehler",
"link_copied": "Link in die Zwischenablage kopiert",
"minutes": "Minuten",
"hours": "Stunden",
"days": "Tage",
"weeks": "Wochen",
"months": "Monate",
"seconds": "Sekunden",
// Dark Mode Toggle
"dark_mode_toggle": "Dunkler Modus",
"light_mode_toggle": "Heller Modus",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Administrationsbereich",
"user_panel": "Benutzerbereich",
"trash_restore_delete": "Papierkorb wiederherstellen/löschen",
"totp_settings": "TOTP-Einstellungen",
"enable_totp": "TOTP aktivieren",
"language": "Sprache",
"select_language": "Sprache auswählen",
"english": "Englisch",
"spanish": "Spanisch",
"french": "Französisch",
"german": "Deutsch",
"use_totp_code_instead": "Stattdessen TOTP-Code verwenden",
"submit_recovery_code": "Wiederherstellungscode absenden",
"please_enter_recovery_code": "Bitte geben Sie Ihren Wiederherstellungscode ein.",
"recovery_code_verification_failed": "Überprüfung des Wiederherstellungscodes fehlgeschlagen",
"error_verifying_recovery_code": "Fehler bei der Überprüfung des Wiederherstellungscodes",
"totp_verification_failed": "TOTP-Überprüfung fehlgeschlagen",
"error_verifying_totp_code": "Fehler bei der Überprüfung des TOTP-Codes",
"totp_setup": "TOTP-Einrichtung",
"scan_qr_code": "Scannen Sie diesen QR-Code mit Ihrer Authenticator-App.",
"enter_totp_confirmation": "Geben Sie den 6-stelligen Code aus Ihrer App zur Bestätigung ein:",
"confirm": "Bestätigen",
"please_enter_valid_code": "Bitte geben Sie einen gültigen 6-stelligen Code ein.",
"totp_enabled_successfully": "TOTP wurde erfolgreich aktiviert.",
"error_generating_recovery_code": "Fehler beim Generieren des Wiederherstellungscodes",
"error_loading_qr_code": "Fehler beim Laden des QR-Codes.",
"error_disabling_totp_setting": "Fehler beim Deaktivieren der TOTP-Einstellungen",
"user_management": "Benutzerverwaltung",
"add_user": "Benutzer hinzufügen",
"remove_user": "Benutzer entfernen",
"user_permissions": "Benutzerberechtigungen",
"oidc_configuration": "OIDC-Konfiguration",
"oidc_provider_url": "OIDC-Anbieter-URL",
"oidc_client_id": "OIDC-Client-ID",
"oidc_client_secret": "OIDC-Client-Geheimnis",
"oidc_redirect_uri": "OIDC-Umleitungs-URI",
"global_totp_settings": "Globale TOTP-Einstellungen",
"global_otpauth_url": "Globale OTPAuth-URL",
"login_options": "Anmeldeoptionen",
"disable_login_form": "Anmeldeformular deaktivieren",
"disable_basic_http_auth": "HTTP-Basisauthentifizierung deaktivieren",
"disable_oidc_login": "OIDC-Anmeldung deaktivieren",
"save_settings": "Einstellungen speichern",
"at_least_one_login_method": "Mindestens eine Anmeldemethode muss aktiviert bleiben.",
"settings_updated_successfully": "Einstellungen wurden erfolgreich aktualisiert.",
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
"user_permissions_updated_successfully": "Benutzerberechtigungen wurden erfolgreich aktualisiert.",
"error_updating_permissions": "Fehler beim Aktualisieren der Berechtigungen",
"no_users_found": "Keine Benutzer gefunden.",
"user_folder_only": "Nur Benutzerordner",
"read_only": "Nur Lesen",
"disable_upload": "Upload deaktivieren",
"error_loading_users": "Fehler beim Laden der Benutzer",
"save_permissions": "Berechtigungen speichern",
"your_recovery_code": "Ihr Wiederherstellungscode",
"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",
"columns": "Spalten"
}
};
let currentLocale = 'en';
export function setLocale(locale) {
currentLocale = locale;
}
export function t(key, placeholders) {
const localeTranslations = translations[currentLocale] || {};
let translation = localeTranslations[key] || key;
if (placeholders) {
Object.keys(placeholders).forEach(ph => {
translation = translation.replace(`{${ph}}`, placeholders[ph]);
});
}
return translation;
}
export function applyTranslations() {
document.querySelectorAll('[data-i18n-key]').forEach(el => {
el.innerText = t(el.getAttribute('data-i18n-key'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder')));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.setAttribute('title', t(el.getAttribute('data-i18n-title')));
});
}

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");
@@ -128,18 +131,18 @@ document.addEventListener("DOMContentLoaded", function () {
if (darkModeToggle) {
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
? "Light Mode"
: "Dark Mode";
? t("light_mode")
: t("dark_mode");
darkModeToggle.addEventListener("click", function () {
if (document.body.classList.contains("dark-mode")) {
document.body.classList.remove("dark-mode");
localStorage.setItem("darkMode", "false");
darkModeToggle.textContent = "Dark Mode";
darkModeToggle.textContent = t("dark_mode");
} else {
document.body.classList.add("dark-mode");
localStorage.setItem("darkMode", "true");
darkModeToggle.textContent = "Light Mode";
darkModeToggle.textContent = t("light_mode");
}
});
}

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

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