improvements and new features see changelog
This commit is contained in:
68
README.md
68
README.md
@@ -8,35 +8,63 @@ Multi File Upload Editor is a lightweight, secure web application for uploading,
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
# Features
|
||||
|
||||
- **Multiple File/Folder Uploads with Progress:**
|
||||
- Users can select and upload multiple files & folders at once. Each file upload shows an individual progress bar with percentage and upload speed, and image files display a small thumbnail preview (default icons for other file types).
|
||||
- Users can select and upload multiple files & folders at once.
|
||||
- Each file upload displays an individual progress bar with percentage and upload speed.
|
||||
- Image files show a small thumbnail preview (with default Material icons for other file types).
|
||||
- **Built-in File Editing & Renaming:**
|
||||
- Text-based files (e.g., .txt, .html, .js) can be opened and edited in a modal window without leaving the page. The editor modal is resizable and now uses CodeMirror for syntax highlighting, line numbering, and zoom in/out functionality—allowing users to adjust the text size for a better editing experience. Files can also be renamed via a dedicated “Rename” action without needing to re-upload them.
|
||||
- Text-based files (e.g., .txt, .html, .js) can be opened and edited in a modal window using CodeMirror for:
|
||||
- Syntax highlighting
|
||||
- Line numbering
|
||||
- Adjustable font sizes
|
||||
- Files can be renamed directly through the interface.
|
||||
- The renaming functionality now supports names with parentheses and checks for duplicate names, automatically generating a unique name (e.g., appending “ (1)”) when needed.
|
||||
- Folder-specific metadata is updated accordingly.
|
||||
- **Built-in File Preview:**
|
||||
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page. The modal maintains a responsive, centered layout that scales with the content. Different Material icons—image for photos, videocam for videos, and picture_as_pdf for PDFs—provide clear visual cues, and custom CSS (including fine-tuning with negative margins) ensures that icons are perfectly aligned. PDFs are embedded at optimal dimensions for a clear, readable view, while video previews include built-in playback controls.
|
||||
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page.
|
||||
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
|
||||
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
|
||||
- **Gallery (Grid) View:**
|
||||
- In addition to the traditional table view, users can toggle to a gallery view that arranges image thumbnails in a grid layout.
|
||||
- The gallery view offers multiple column options (e.g., 3, 4, or 5 columns) so that users can choose the layout that best fits their screen.
|
||||
- Action buttons (Download, Edit, Rename, Share) appear beneath each thumbnail for quick access.
|
||||
- **Batch Operations (Delete/Copy/Move/Download):**
|
||||
- Delete Files: Delete multiple files at once.
|
||||
- Copy Files: Copy selected files to another folder.
|
||||
- Move Files: Move selected files to a different folder.
|
||||
- Download Files as ZIP: Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
||||
- **Delete Files:** Delete multiple files at once.
|
||||
- **Copy Files:** Copy selected files to another folder with a unique-naming feature to prevent overwrites.
|
||||
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
||||
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
||||
- **Folder Management:**
|
||||
- Supports organizing files into folders and subfolders. Users can create new folders, rename existing folders, or delete folders. A dynamic folder tree in the UI allows navigation through directories and updates in real-time to reflect changes after any create, rename, or delete action.
|
||||
- Organize files into folders and subfolders with the ability to create, rename, and delete folders.
|
||||
- A dynamic folder tree in the UI allows users to navigate directories easily, and any changes are immediately reflected in real time.
|
||||
- **Per-Folder Metadata Storage:** Each folder has its own metadata JSON file (e.g., `root_metadata.json`, `FolderName_metadata.json`), and operations (copy/move/rename) update these metadata files accordingly.
|
||||
- **Sorting & Pagination:**
|
||||
- The file list can be sorted by name, last modified date, upload date, size, or uploader. For easier browsing, the interface supports pagination with selectable page sizes (10, 20, 50, or 100 items per page) and navigation controls (“Prev”, “Next”, specific page numbers).
|
||||
- The file list can be sorted by name, modified date, upload date, file size, or uploader.
|
||||
- Pagination controls let users navigate through files with selectable page sizes (10, 20, 50, or 100 items per page) and “Prev”/“Next” navigation buttons.
|
||||
- **Share Link Functionality:**
|
||||
- Generate shareable links for files with configurable expiration times (e.g., 30, 60, 120, 180, 240 minutes, and a 1-day option) and optional password protection.
|
||||
- Share links are stored in a JSON file with details including the folder, file, expiration timestamp, and hashed password.
|
||||
- The share endpoint (`share.php`) validates tokens, expiration, and password before serving files (or forcing downloads).
|
||||
- The share URL is configurable via environment variables or auto-detected from the server.
|
||||
- **User Authentication & Management:**
|
||||
- Secure, session-based authentication protects the editor. An admin user can add or remove users through the interface. Passwords are hashed using PHP’s password_hash() for security, and session checks prevent unauthorized access to backend endpoints.
|
||||
- **CSRF Protection:** All state-changing endpoints (such as those for folder and file operations) include CSRF token validation to ensure that only legitimate requests from authenticated users are processed.
|
||||
- Secure, session-based authentication protects the file manager.
|
||||
- Admin users can add or remove users through the interface.
|
||||
- Passwords are hashed using PHP’s `password_hash()` for security.
|
||||
- All state-changing endpoints include CSRF token validation.
|
||||
- **Responsive, Dynamic & Persistent UI:**
|
||||
- The interface is mobile-friendly and adjusts to different screen sizes (hiding non-critical columns on small devices to avoid clutter). Updates to the file list, folder tree, and upload progress happen asynchronously (via Fetch API and XMLHttpRequest), so the page never needs to fully reload. Users receive immediate feedback through toast notifications and modal dialogs for actions like confirmations and error messages, creating a smooth user experience. Persistent UI elements Items Per Page, Dark/Light Mode, folder tree view & last open folder.
|
||||
- **Dark Mode/Light Mode**
|
||||
- Automatically adapts to the operating system’s theme preference by default, with a manual toggle option.
|
||||
- A theme toggle allows users to switch between Dark Mode and Light Mode for an optimized viewing experience.
|
||||
- Every element, including the header, buttons, tables, modals, and the file editor, dynamically adapts to the selected theme.
|
||||
- Dark Mode: Uses a dark gray background with lighter text to reduce eye strain in low-light environments.
|
||||
- Light Mode: Retains the classic bright interface for high visibility in well-lit conditions.
|
||||
- CodeMirror editor applies a matching dark theme in Dark Mode for better readability when editing files.
|
||||
- The interface is mobile-friendly and adapts to various screen sizes by hiding non-critical columns on small devices.
|
||||
- Asynchronous updates (via Fetch API and XMLHttpRequest) keep the UI responsive without full page reloads.
|
||||
- Persistent settings (such as items per page, dark/light mode preference, folder tree state, and the last open folder) ensure a smooth and customized user experience.
|
||||
- **Dark Mode/Light Mode:**
|
||||
- The application automatically adapts to the operating system’s theme preference by default and offers a manual toggle.
|
||||
- The dark mode provides a darker background with lighter text and adjusts UI elements (including the CodeMirror editor) for optimal readability in low-light conditions.
|
||||
- The light mode maintains a bright interface for well-lit environments.
|
||||
- **Server & Security Enhancements:**
|
||||
- The Apache configuration (or .htaccess files) is set to disable directory indexing (e.g., using `Options -Indexes` in the uploads directory), preventing unauthorized users from viewing directory contents.
|
||||
- Direct access to sensitive files (e.g., `users.txt`) is restricted through .htaccess rules.
|
||||
- A proxy download mechanism has been implemented (via endpoints like `download.php` and `downloadZip.php`) so that every file download request goes through a PHP script. This script validates the session and CSRF token before streaming the file, ensuring that even if a file URL is guessed, only authenticated users can access it.
|
||||
- Administrators are advised to deploy the app on a secure internal network or use the proxy download mechanism for public deployments to further protect file content.
|
||||
|
||||
---
|
||||
|
||||
|
||||
1
auth.js
1
auth.js
@@ -19,7 +19,6 @@ function initAuth() {
|
||||
// Include CSRF token header with login
|
||||
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(data => {
|
||||
console.log("Login response:", data);
|
||||
if (data.success) {
|
||||
console.log("✅ Login successful. Reloading page.");
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
||||
|
||||
13
auth.php
13
auth.php
@@ -3,16 +3,10 @@ require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
/*$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;
|
||||
}*/
|
||||
|
||||
// Function to authenticate user
|
||||
function authenticate($username, $password) {
|
||||
function authenticate($username, $password)
|
||||
{
|
||||
global $usersFile;
|
||||
|
||||
if (!file_exists($usersFile)) {
|
||||
@@ -49,6 +43,8 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
// Authenticate user
|
||||
$userRole = authenticate($username, $password);
|
||||
if ($userRole !== false) {
|
||||
// Regenerate session ID to mitigate session fixation attacks
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
|
||||
@@ -57,4 +53,3 @@ if ($userRole !== false) {
|
||||
} else {
|
||||
echo json_encode(["error" => "Invalid credentials"]);
|
||||
}
|
||||
?>
|
||||
42
config.php
42
config.php
@@ -1,13 +1,48 @@
|
||||
<?php
|
||||
session_set_cookie_params(7200); // 2 hours in seconds
|
||||
// config.php
|
||||
|
||||
// Allow an environment variable to override HTTPS detection.
|
||||
$envSecure = getenv('SECURE');
|
||||
if ($envSecure !== false) {
|
||||
// Convert the environment variable value to a boolean.
|
||||
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
|
||||
$cookieParams = [
|
||||
'lifetime' => 7200,
|
||||
'path' => '/',
|
||||
'domain' => '', // Specify your domain if needed
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
session_set_cookie_params($cookieParams);
|
||||
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
// config.php
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
|
||||
// Define BASE_URL (this should point to where index.html is, e.g. your uploads directory)
|
||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||
|
||||
// If BASE_URL is still the default placeholder, use the server's HTTP_HOST.
|
||||
// Otherwise, use BASE_URL and append share.php.
|
||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
||||
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
||||
: "http://localhost/share.php";
|
||||
} else {
|
||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||
}
|
||||
|
||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
@@ -15,5 +50,6 @@ define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
define('META_DIR','/var/www/metadata/');
|
||||
define('META_FILE','file_metadata.json');
|
||||
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
?>
|
||||
@@ -5,7 +5,6 @@ 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);
|
||||
@@ -58,54 +57,82 @@ $destDir = ($destinationFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Load metadata.
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
$basename = basename(trim($fileName));
|
||||
// Validate the file name.
|
||||
// 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 . $basename;
|
||||
$srcPath = $sourceDir . $originalName;
|
||||
$destPath = $destDir . $basename;
|
||||
|
||||
// Build metadata keys.
|
||||
$srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename;
|
||||
$destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename;
|
||||
|
||||
clearstatcache();
|
||||
if (!file_exists($srcPath)) {
|
||||
$errors[] = "$basename does not exist in source.";
|
||||
$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 metadata: if source key exists, duplicate it to destination key.
|
||||
if (isset($metadata[$srcKey])) {
|
||||
$metadata[$destKey] = $metadata[$srcKey];
|
||||
|
||||
// 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($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT))) {
|
||||
$errors[] = "Failed to update metadata.";
|
||||
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update destination metadata.";
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
|
||||
@@ -50,8 +50,10 @@ if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) {
|
||||
$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.
|
||||
@@ -62,6 +64,21 @@ if (file_exists($fullPath)) {
|
||||
|
||||
// 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.']);
|
||||
|
||||
59
createShareLink.php
Normal file
59
createShareLink.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
$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('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optionally, you could check if the file exists in the uploads directory here.
|
||||
|
||||
// Generate a secure token.
|
||||
$token = bin2hex(random_bytes(4)); // 8 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 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."]);
|
||||
}
|
||||
?>
|
||||
@@ -19,6 +19,15 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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 request body
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
@@ -50,7 +59,7 @@ $deletedFiles = [];
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
$basename = basename(trim($fileName));
|
||||
@@ -65,13 +74,27 @@ foreach ($data['files'] as $fileName) {
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
if (unlink($filePath)) {
|
||||
$deletedFiles[] = $fileName;
|
||||
$deletedFiles[] = $basename;
|
||||
} else {
|
||||
$errors[] = "Failed to delete $fileName";
|
||||
$errors[] = "Failed to delete $basename";
|
||||
}
|
||||
} else {
|
||||
// Consider file already deleted.
|
||||
$deletedFiles[] = $fileName;
|
||||
$deletedFiles[] = $basename;
|
||||
}
|
||||
}
|
||||
|
||||
// Update folder-specific metadata file by removing deleted files.
|
||||
$metadataFile = getMetadataFilePath($folder);
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
if (is_array($metadata)) {
|
||||
foreach ($deletedFiles as $delFile) {
|
||||
if (isset($metadata[$delFile])) {
|
||||
unset($metadata[$delFile]);
|
||||
}
|
||||
}
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,28 @@ if (count(scandir($folderPath)) > 2) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Attempt to delete the folder
|
||||
/**
|
||||
* 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.']);
|
||||
|
||||
52
domUtils.js
52
domUtils.js
@@ -141,30 +141,44 @@ export function buildFileTableRow(file, folderPath) {
|
||||
} else if (/\.pdf$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
||||
}
|
||||
previewButton = `<button class="btn btn-sm btn-info ml-2 preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
||||
<td>
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
||||
</td>
|
||||
<td>${safeFileName}</td>
|
||||
<td class="hide-small nowrap">${safeModified}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploaded}</td>
|
||||
<td class="hide-small nowrap">${safeSize}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||
<td>
|
||||
<div class="button-wrap">
|
||||
<a class="btn btn-sm btn-success ml-2" href="${folderPath + encodeURIComponent(file.name)}" download>Download</a>
|
||||
${file.editable ? `<button class="btn btn-sm btn-primary ml-2" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'>Edit</button>` : ""}
|
||||
${previewButton}
|
||||
<button class="btn btn-sm btn-warning ml-2" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'>Rename</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
||||
<td>
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
||||
</td>
|
||||
<td>${safeFileName}</td>
|
||||
<td class="hide-small nowrap">${safeModified}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploaded}</td>
|
||||
<td class="hide-small nowrap">${safeSize}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||
<td>
|
||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||
<a class="btn btn-sm btn-success download-btn"
|
||||
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
|
||||
title="Download">
|
||||
<i class="material-icons">file_download</i>
|
||||
</a>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm btn-primary"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="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">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
59
download.php
Normal file
59
download.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// For GET requests (which download.php will use), we assume session authentication is enough.
|
||||
|
||||
// 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('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine the directory.
|
||||
if ($folder !== 'root') {
|
||||
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$directory = UPLOAD_DIR;
|
||||
}
|
||||
|
||||
$filePath = $directory . $file;
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "File not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve the file.
|
||||
$mimeType = mime_content_type($filePath);
|
||||
header("Content-Type: " . $mimeType);
|
||||
|
||||
// For images, serve inline; for other types, force download.
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
|
||||
} else {
|
||||
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
|
||||
}
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
|
||||
// Disable caching.
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
readfile($filePath);
|
||||
exit;
|
||||
?>
|
||||
@@ -38,7 +38,7 @@ $files = $data['files'];
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-. ]+$/', $part)) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
@@ -76,7 +76,7 @@ if (empty($files)) {
|
||||
}
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
if (!preg_match('/^[A-Za-z0-9_\-. ]+$/', $fileName)) {
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
@@ -119,10 +119,14 @@ foreach ($filesToZip as $filePath) {
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// Serve the ZIP file.
|
||||
// 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;
|
||||
|
||||
397
fileManager.js
397
fileManager.js
@@ -9,7 +9,7 @@ import {
|
||||
showToast,
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
previewFile
|
||||
previewFile as originalPreviewFile
|
||||
} from './domUtils.js';
|
||||
|
||||
export let fileData = [];
|
||||
@@ -17,8 +17,37 @@ export let sortOrder = { column: "uploaded", ascending: true };
|
||||
|
||||
window.itemsPerPage = window.itemsPerPage || 10;
|
||||
window.currentPage = window.currentPage || 1;
|
||||
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
|
||||
|
||||
// ==============================
|
||||
// VIEW MODE TOGGLE BUTTON
|
||||
// ==============================
|
||||
function createViewToggleButton() {
|
||||
let toggleBtn = document.getElementById("toggleViewBtn");
|
||||
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.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "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" ? "Switch to Table View" : "Switch to Gallery View";
|
||||
};
|
||||
return toggleBtn;
|
||||
}
|
||||
window.createViewToggleButton = createViewToggleButton;
|
||||
|
||||
// -----------------------------
|
||||
// Helper: formatFolderName
|
||||
// -----------------------------
|
||||
|
||||
// --- Define formatFolderName ---
|
||||
function formatFolderName(folder) {
|
||||
if (folder === "root") return "(Root)";
|
||||
return folder
|
||||
@@ -26,11 +55,224 @@ function formatFolderName(folder) {
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
}
|
||||
|
||||
// Expose DOM helper functions for inline handlers.
|
||||
// Expose inline DOM helpers.
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
window.previewFile = previewFile;
|
||||
|
||||
// ==============================================
|
||||
// FEATURE: Public File Sharing Modal
|
||||
// ==============================================
|
||||
|
||||
function openShareModal(file, folder) {
|
||||
const existing = document.getElementById("shareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "shareModal";
|
||||
modal.classList.add("modal");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>Share File: ${escapeHTML(file.name)}</h3>
|
||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Set Expiration:</p>
|
||||
<select id="shareExpiration">
|
||||
<option value="30">30 minutes</option>
|
||||
<option value="60" selected>60 minutes</option>
|
||||
<option value="120">120 minutes</option>
|
||||
<option value="180">180 minutes</option>
|
||||
<option value="240">240 minutes</option>
|
||||
<option value="1440">1 Day</option>
|
||||
</select>
|
||||
<p>Password (optional):</p>
|
||||
<input type="text" id="sharePassword" placeholder="No password by default" style="width: 100%;"/>
|
||||
<br>
|
||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">Generate Share Link</button>
|
||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
||||
<p>Shareable Link:</p>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
||||
const expiration = document.getElementById("shareExpiration").value;
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
fetch("createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: folder,
|
||||
file: file.name,
|
||||
expirationMinutes: parseInt(expiration),
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.token) {
|
||||
// Get the share endpoint from the meta tag (or fallback to a global variable)
|
||||
let shareEndpoint = document.querySelector('meta[name="share-url"]')
|
||||
? document.querySelector('meta[name="share-url"]').getAttribute('content')
|
||||
: (window.SHARE_URL || "share.php");
|
||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
||||
const inputField = document.getElementById("shareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
} else {
|
||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating share link:", err);
|
||||
showToast("Error generating share link.");
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast("Link copied to clipboard!");
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// FEATURE: Enhanced Preview Modal with Navigation
|
||||
// =============================================
|
||||
// This function replaces the previous preview behavior for images.
|
||||
// It uses your original modal layout and, if multiple images exist,
|
||||
// overlays transparent Prev/Next buttons over the image.
|
||||
function enhancedPreviewFile(fileUrl, fileName) {
|
||||
let modal = document.getElementById("filePreviewModal");
|
||||
if (!modal) {
|
||||
modal = document.createElement("div");
|
||||
modal.id = "filePreviewModal";
|
||||
Object.assign(modal.style, {
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: "1000"
|
||||
});
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh;">
|
||||
<span id="closeFileModal" class="close-image-modal" style="position: absolute; top: 10px; right: 10px; font-size: 24px; cursor: pointer;">×</span>
|
||||
<h4 class="image-modal-header" style="text-align: center; margin-top: 40px;"></h4>
|
||||
<div class="file-preview-container" style="position: relative; text-align: center;"></div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById("closeFileModal").addEventListener("click", function () {
|
||||
modal.style.display = "none";
|
||||
});
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
modal.querySelector("h4").textContent = fileName;
|
||||
const container = modal.querySelector(".file-preview-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName);
|
||||
if (isImage) {
|
||||
const img = document.createElement("img");
|
||||
img.src = fileUrl;
|
||||
img.className = "image-modal-img";
|
||||
img.style.maxWidth = "80vw";
|
||||
img.style.maxHeight = "80vh";
|
||||
container.appendChild(img);
|
||||
|
||||
// If multiple images exist, add arrow 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);
|
||||
|
||||
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.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length;
|
||||
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
|
||||
modal.querySelector("h4").textContent = newFile.name;
|
||||
img.src = ((window.currentFolder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
|
||||
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
|
||||
});
|
||||
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.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length;
|
||||
let newFile = modal.galleryImages[modal.galleryCurrentIndex];
|
||||
modal.querySelector("h4").textContent = newFile.name;
|
||||
img.src = ((window.currentFolder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
|
||||
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
|
||||
});
|
||||
container.appendChild(prevBtn);
|
||||
container.appendChild(nextBtn);
|
||||
}
|
||||
} else {
|
||||
if (extension === "pdf") {
|
||||
const embed = document.createElement("embed");
|
||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
||||
embed.type = "application/pdf";
|
||||
embed.style.width = "80vw";
|
||||
embed.style.height = "80vh";
|
||||
embed.style.border = "none";
|
||||
container.appendChild(embed);
|
||||
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
|
||||
const video = document.createElement("video");
|
||||
video.src = fileUrl;
|
||||
video.controls = true;
|
||||
video.className = "image-modal-img";
|
||||
container.appendChild(video);
|
||||
} else {
|
||||
container.textContent = "Preview not available for this file type.";
|
||||
}
|
||||
}
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
enhancedPreviewFile(fileUrl, fileName);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ORIGINAL FILE MANAGER FUNCTIONS
|
||||
// ==============================================
|
||||
export function loadFileList(folderParam) {
|
||||
const folder = folderParam || "root";
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
@@ -39,7 +281,17 @@ export function loadFileList(folderParam) {
|
||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||
|
||||
return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// Check if the session has expired.
|
||||
if (response.status === 401) {
|
||||
showToast("Session expired. Please log in again.");
|
||||
// Redirect to logout.php to clear the session; this can trigger a login process.
|
||||
window.location.href = "logout.php";
|
||||
// Throw error to stop further processing.
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
fileListContainer.innerHTML = "";
|
||||
if (data.files && data.files.length > 0) {
|
||||
@@ -47,10 +299,17 @@ export function loadFileList(folderParam) {
|
||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||||
file.editable = canEditFile(file.name);
|
||||
file.folder = folder;
|
||||
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
file.type = "image";
|
||||
}
|
||||
return file;
|
||||
});
|
||||
fileData = data.files;
|
||||
renderFileTable(folder);
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
renderFileTable(folder);
|
||||
}
|
||||
} else {
|
||||
fileListContainer.textContent = "No files found.";
|
||||
updateFileActionButtons();
|
||||
@@ -59,7 +318,10 @@ export function loadFileList(folderParam) {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error loading file list:", error);
|
||||
fileListContainer.textContent = "Error loading files.";
|
||||
// Only update the container text if error is not due to an unauthorized response.
|
||||
if (error.message !== "Unauthorized") {
|
||||
fileListContainer.textContent = "Error loading files.";
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -95,7 +357,8 @@ export function renderFileTable(folder) {
|
||||
searchTerm
|
||||
});
|
||||
|
||||
const headerHTML = buildFileTableHeader(sortOrder);
|
||||
let headerHTML = buildFileTableHeader(sortOrder);
|
||||
// Do not add a separate share column; share button goes into the actions cell.
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
||||
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
||||
@@ -103,10 +366,15 @@ export function renderFileTable(folder) {
|
||||
let rowsHTML = "<tbody>";
|
||||
if (totalFiles > 0) {
|
||||
filteredFiles.slice(startIndex, endIndex).forEach(file => {
|
||||
rowsHTML += buildFileTableRow(file, folderPath);
|
||||
let rowHTML = buildFileTableRow(file, folderPath);
|
||||
// Insert share button into the actions container.
|
||||
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
|
||||
<i class="material-icons">share</i>
|
||||
</button>$1`);
|
||||
rowsHTML += rowHTML;
|
||||
});
|
||||
} else {
|
||||
rowsHTML += `<tr><td colspan="7">No files found.</td></tr>`;
|
||||
rowsHTML += `<tr><td colspan="8">No files found.</td></tr>`;
|
||||
}
|
||||
rowsHTML += "</tbody></table>";
|
||||
|
||||
@@ -114,6 +382,8 @@ export function renderFileTable(folder) {
|
||||
|
||||
fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
createViewToggleButton();
|
||||
|
||||
const newSearchInput = document.getElementById("searchInput");
|
||||
if (newSearchInput) {
|
||||
newSearchInput.addEventListener("input", debounce(function () {
|
||||
@@ -141,6 +411,78 @@ export function renderFileTable(folder) {
|
||||
});
|
||||
});
|
||||
|
||||
// Bind share button events in table view.
|
||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const fileName = this.getAttribute("data-file");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
if (file) {
|
||||
openShareModal(file, folder);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
export function renderGalleryView(folder) {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
const folderPath = (folder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
// Use CSS Grid for gallery layout.
|
||||
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}">`;
|
||||
fileData.forEach((file, index) => {
|
||||
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;">`;
|
||||
} else {
|
||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||
}
|
||||
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;">${escapeHTML(file.name)}</span>
|
||||
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
||||
<a class="btn btn-sm btn-success download-btn"
|
||||
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
|
||||
title="Download">
|
||||
<i class="material-icons">file_download</i>
|
||||
</a>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm btn-primary" 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" onclick='openShareModal(${JSON.stringify(file)}, ${JSON.stringify(folder)})' title="Share">
|
||||
<i class="material-icons">share</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
galleryHTML += "</div>";
|
||||
fileListContainer.innerHTML = galleryHTML;
|
||||
|
||||
document.querySelectorAll(".gallery-share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const fileName = this.getAttribute("data-file");
|
||||
const folder = this.getAttribute("data-folder");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
if (file) {
|
||||
openShareModal(file, folder);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
@@ -167,7 +509,11 @@ export function sortFiles(column, folder) {
|
||||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
renderFileTable(folder);
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
renderFileTable(folder);
|
||||
}
|
||||
}
|
||||
|
||||
function parseCustomDate(dateStr) {
|
||||
@@ -355,8 +701,12 @@ export function handleCopySelected(e) {
|
||||
export async function loadCopyMoveFolderListForModal(dropdownId) {
|
||||
try {
|
||||
const response = await fetch('getFolderList.php');
|
||||
const folders = await response.json();
|
||||
console.log('Folders fetched for modal:', folders);
|
||||
let folders = await response.json();
|
||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
||||
folders = folders.map(item => item.folder);
|
||||
}
|
||||
folders = folders.filter(folder => folder !== "root");
|
||||
|
||||
const folderSelect = document.getElementById(dropdownId);
|
||||
folderSelect.innerHTML = '';
|
||||
const rootOption = document.createElement('option');
|
||||
@@ -389,11 +739,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
confirmCopy.addEventListener("click", function () {
|
||||
const targetFolder = document.getElementById("copyTargetFolder").value;
|
||||
if (!targetFolder) {
|
||||
showToast("Please select a target folder for copying.!", 5000);
|
||||
showToast("Please select a target folder for copying.", 5000);
|
||||
return;
|
||||
}
|
||||
if (targetFolder === window.currentFolder) {
|
||||
showToast("Error: Cannot move files to the same folder.");
|
||||
showToast("Error: Cannot copy files to the same folder.");
|
||||
return;
|
||||
}
|
||||
fetch("copyFiles.php", {
|
||||
@@ -403,7 +753,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ source: window.currentFolder, files: window.filesToCopy, destination: targetFolder })
|
||||
body: JSON.stringify({
|
||||
source: window.currentFolder,
|
||||
files: window.filesToCopy,
|
||||
destination: targetFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -463,7 +817,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ source: window.currentFolder, files: window.filesToMove, destination: targetFolder })
|
||||
body: JSON.stringify({
|
||||
source: window.currentFolder,
|
||||
files: window.filesToMove,
|
||||
destination: targetFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -660,7 +1018,7 @@ export function saveFile(fileName, folder) {
|
||||
|
||||
export function displayFilePreview(file, container) {
|
||||
container.style.display = "inline-block";
|
||||
if (file.type.startsWith("image/")) {
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
const img = document.createElement("img");
|
||||
img.src = URL.createObjectURL(file);
|
||||
img.classList.add("file-preview-img");
|
||||
@@ -761,4 +1119,5 @@ window.changeItemsPerPage = function (newCount) {
|
||||
window.itemsPerPage = parseInt(newCount);
|
||||
window.currentPage = 1;
|
||||
renderFileTable(window.currentFolder);
|
||||
};
|
||||
};
|
||||
window.previewFile = previewFile;
|
||||
@@ -9,6 +9,7 @@ import { showToast, escapeHTML } from './domUtils.js';
|
||||
|
||||
// Formats a folder name for display (e.g. adding indentations).
|
||||
export function formatFolderName(folder) {
|
||||
if (typeof folder !== "string") return "";
|
||||
if (folder.indexOf("/") !== -1) {
|
||||
let parts = folder.split("/");
|
||||
let indent = "";
|
||||
@@ -25,6 +26,8 @@ export function formatFolderName(folder) {
|
||||
function buildFolderTree(folders) {
|
||||
const tree = {};
|
||||
folders.forEach(folderPath => {
|
||||
// Ensure folderPath is a string
|
||||
if (typeof folderPath !== "string") return;
|
||||
const parts = folderPath.split('/');
|
||||
let current = tree;
|
||||
parts.forEach(part => {
|
||||
@@ -119,9 +122,20 @@ export async function loadFolderTree(selectedFolder) {
|
||||
const response = await fetch('getFolderList.php');
|
||||
if (response.status === 401) {
|
||||
console.error("Unauthorized: Please log in to view folders.");
|
||||
showToast("Session expired. Please log in again.");
|
||||
// Redirect to logout.php to clear the session; this can trigger a login process.
|
||||
window.location.href = "logout.php";
|
||||
return;
|
||||
}
|
||||
const folders = await response.json();
|
||||
let folders = await response.json();
|
||||
|
||||
// If returned items are objects (with a "folder" property), extract folder paths.
|
||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
||||
folders = folders.map(item => item.folder);
|
||||
}
|
||||
// Filter out duplicate "root" entries if present.
|
||||
folders = folders.filter(folder => folder !== "root");
|
||||
|
||||
if (!Array.isArray(folders)) {
|
||||
console.error("Folder list response is not an array:", folders);
|
||||
return;
|
||||
@@ -294,7 +308,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderFull })
|
||||
body: JSON.stringify({ oldFolder: window.currentFolder, newFolder: newFolderFull })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
||||
@@ -26,7 +26,22 @@ if ($folder !== 'root') {
|
||||
$directory = UPLOAD_DIR;
|
||||
}
|
||||
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
/**
|
||||
* 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)) {
|
||||
@@ -37,10 +52,15 @@ if (!is_dir($directory)) {
|
||||
$files = array_values(array_diff(scandir($directory), array('.', '..')));
|
||||
$fileList = [];
|
||||
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
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;
|
||||
@@ -50,8 +70,8 @@ foreach ($files as $file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build the metadata key; if not in root, include the folder path.
|
||||
$metaKey = ($folder !== 'root') ? $folder . "/" . $file : $file;
|
||||
// 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";
|
||||
|
||||
@@ -23,7 +23,6 @@ function getSubfolders($dir, $relative = '') {
|
||||
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
// Only process folder names that match the safe pattern.
|
||||
if (!preg_match($safeFolderNamePattern, $item)) {
|
||||
continue;
|
||||
}
|
||||
@@ -40,12 +39,59 @@ function getSubfolders($dir, $relative = '') {
|
||||
return $folders;
|
||||
}
|
||||
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderList = [];
|
||||
|
||||
if (is_dir($baseDir)) {
|
||||
$folderList = getSubfolders($baseDir);
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
|
||||
echo json_encode($folderList);
|
||||
$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);
|
||||
?>
|
||||
@@ -7,7 +7,8 @@
|
||||
<title>Multi File Upload Editor</title>
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="" />
|
||||
<meta name="csrf-token" content="">
|
||||
<meta name="share-url" content="">
|
||||
<!-- External CSS -->
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<!-- Google Fonts and Material Icons -->
|
||||
@@ -174,7 +175,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
|
||||
<i class="material-icons">edit</i>
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<!-- Rename Folder Modal -->
|
||||
<div id="renameFolderModal" class="modal">
|
||||
|
||||
31
main.js
31
main.js
@@ -17,25 +17,38 @@ import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
|
||||
|
||||
function loadCsrfToken() {
|
||||
fetch('token.php', { credentials: 'include' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Assign to global variable
|
||||
// Set global variables.
|
||||
window.csrfToken = data.csrf_token;
|
||||
// Also update the meta tag
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
// Update (or create) the CSRF meta tag.
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
}
|
||||
meta.setAttribute('content', data.csrf_token);
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
|
||||
// Update (or create) the share URL meta tag.
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
})
|
||||
.catch(error => console.error("Error loading CSRF token:", error));
|
||||
.catch(error => console.error("Error loading CSRF token and share URL:", error));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadCsrfToken);
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
// --- 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);
|
||||
@@ -30,7 +32,6 @@ if (
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get and trim folder parameters.
|
||||
$sourceFolder = trim($data['source']) ?: 'root';
|
||||
$destinationFolder = trim($data['destination']) ?: 'root';
|
||||
|
||||
@@ -51,17 +52,13 @@ $destinationFolder = trim($destinationFolder, "/\\ ");
|
||||
|
||||
// Build the source and destination directories.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$sourceDir = ($sourceFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
$sourceDir = ($sourceFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
|
||||
$destDir = ($destinationFolder === 'root')
|
||||
? $baseDir . DIRECTORY_SEPARATOR
|
||||
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Load metadata.
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
|
||||
|
||||
// Ensure destination directory exists.
|
||||
if (!is_dir($destDir)) {
|
||||
if (!mkdir($destDir, 0775, true)) {
|
||||
@@ -70,42 +67,87 @@ if (!is_dir($destDir)) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
// Define a safe pattern for file names: letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
$basename = basename($fileName);
|
||||
// Validate file name.
|
||||
// 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 . $basename;
|
||||
$srcPath = $sourceDir . $originalName;
|
||||
$destPath = $destDir . $basename;
|
||||
|
||||
// Build metadata keys.
|
||||
$srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename;
|
||||
$destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename;
|
||||
|
||||
clearstatcache();
|
||||
if (!file_exists($srcPath)) {
|
||||
$errors[] = "$basename does not exist in source.";
|
||||
$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: copy source metadata to destination key and remove source key.
|
||||
if (isset($metadata[$srcKey])) {
|
||||
$metadata[$destKey] = $metadata[$srcKey];
|
||||
unset($metadata[$srcKey]);
|
||||
|
||||
// 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($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT))) {
|
||||
$errors[] = "Failed to update metadata.";
|
||||
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)) {
|
||||
|
||||
@@ -38,8 +38,8 @@ if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
$oldName = basename(trim($data['oldName']));
|
||||
$newName = basename(trim($data['newName']));
|
||||
|
||||
// Validate file names: allow letters, numbers, underscores, dashes, dots, and spaces.
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\. ]+$/', $oldName) || !preg_match('/^[A-Za-z0-9_\-\. ]+$/', $newName)) {
|
||||
// Validate file names: allow letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $oldName) || !preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $newName)) {
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
@@ -54,32 +54,50 @@ if ($folder !== 'root') {
|
||||
$oldPath = $directory . $oldName;
|
||||
$newPath = $directory . $newName;
|
||||
|
||||
// Helper: Generate a unique file name if a file with the same name exists.
|
||||
function getUniqueFileName($directory, $fileName) {
|
||||
$fullPath = $directory . $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 = $directory . $newName;
|
||||
clearstatcache(true, $newFullPath);
|
||||
$counter++;
|
||||
} while (file_exists($directory . $newName));
|
||||
return $newName;
|
||||
}
|
||||
|
||||
if (!file_exists($oldPath)) {
|
||||
echo json_encode(["error" => "File does not exist"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If a file with the new name exists, generate a unique name.
|
||||
if (file_exists($newPath)) {
|
||||
echo json_encode(["error" => "A file with the new name already exists"]);
|
||||
exit;
|
||||
$newName = getUniqueFileName($directory, $newName);
|
||||
$newPath = $directory . $newName;
|
||||
}
|
||||
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata.
|
||||
// --- Update Metadata in the Folder-Specific JSON ---
|
||||
$metadataKey = ($folder === 'root') ? "root" : $folder;
|
||||
$metadataFile = META_DIR . str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
// Build metadata keys using the folder (if not root).
|
||||
$oldKey = ($folder !== 'root') ? $folder . "/" . $oldName : $oldName;
|
||||
$newKey = ($folder !== 'root') ? $folder . "/" . $newName : $newName;
|
||||
if (isset($metadata[$oldKey])) {
|
||||
$metadata[$newKey] = $metadata[$oldKey];
|
||||
unset($metadata[$oldKey]);
|
||||
if (isset($metadata[$oldName])) {
|
||||
$metadata[$newName] = $metadata[$oldName];
|
||||
unset($metadata[$oldName]);
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
echo json_encode(["success" => "File renamed successfully"]);
|
||||
echo json_encode(["success" => "File renamed successfully", "newName" => $newName]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Error renaming file"]);
|
||||
}
|
||||
|
||||
@@ -70,28 +70,21 @@ if (file_exists($newPath)) {
|
||||
|
||||
// Attempt to rename the folder.
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata.
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
$updated = false;
|
||||
// Loop through each key in the metadata.
|
||||
foreach ($metadata as $key => $value) {
|
||||
// Check if the key is the folder itself or is inside the folder.
|
||||
if ($key === $oldFolder || strpos($key, $oldFolder . "/") === 0) {
|
||||
// Construct the new key by replacing the $oldFolder prefix with $newFolder.
|
||||
$newKey = $newFolder . substr($key, strlen($oldFolder));
|
||||
// Optional: remove a leading slash if it appears.
|
||||
$newKey = ltrim($newKey, "/");
|
||||
$metadata[$newKey] = $value;
|
||||
unset($metadata[$key]);
|
||||
$updated = true;
|
||||
}
|
||||
}
|
||||
if ($updated) {
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
}
|
||||
// --- Update Metadata Files ---
|
||||
// Generate a metadata prefix for the old folder path and new folder path.
|
||||
$oldPrefix = str_replace(['/', '\\', ' '], '-', $oldFolder);
|
||||
$newPrefix = str_replace(['/', '\\', ' '], '-', $newFolder);
|
||||
|
||||
// Find all metadata files whose names start with the old prefix.
|
||||
$metadataFiles = glob(META_DIR . $oldPrefix . '*_metadata.json');
|
||||
foreach ($metadataFiles as $oldMetaFile) {
|
||||
$baseName = basename($oldMetaFile);
|
||||
// Replace the old prefix with the new prefix in the filename.
|
||||
$newBaseName = preg_replace('/^' . preg_quote($oldPrefix, '/') . '/', $newPrefix, $baseName);
|
||||
$newMetaFile = META_DIR . $newBaseName;
|
||||
rename($oldMetaFile, $newMetaFile);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to rename folder.']);
|
||||
|
||||
36
saveFile.php
36
saveFile.php
@@ -69,6 +69,42 @@ $filePath = $targetDir . $fileName;
|
||||
|
||||
// Attempt to save the file.
|
||||
if (file_put_contents($filePath, $data["content"]) !== false) {
|
||||
|
||||
// --- Update Metadata (Using Separate JSON per Folder) ---
|
||||
// Determine the metadata key: for "root", use "root"; otherwise, use the folder path.
|
||||
$metadataKey = (strtolower($folder) === "root" || $folder === "") ? "root" : $folder;
|
||||
// Create a unique metadata filename by replacing slashes, backslashes, and spaces with dashes.
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
$metadataFilePath = META_DIR . $metadataFileName;
|
||||
|
||||
// Load existing metadata for this folder if it exists.
|
||||
if (file_exists($metadataFilePath)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFilePath), true);
|
||||
} else {
|
||||
$metadata = [];
|
||||
}
|
||||
|
||||
$currentTime = date(DATE_TIME_FORMAT);
|
||||
$uploader = $_SESSION['username'] ?? "Unknown";
|
||||
|
||||
// Update metadata for this file.
|
||||
// If the file already exists in metadata, update its "modified" field.
|
||||
if (isset($metadata[$fileName])) {
|
||||
$metadata[$fileName]['modified'] = $currentTime;
|
||||
// Optionally, you might also update the uploader if desired.
|
||||
$metadata[$fileName]['uploader'] = $uploader;
|
||||
} else {
|
||||
// New entry: record both uploaded and modified times.
|
||||
$metadata[$fileName] = [
|
||||
"uploaded" => $currentTime,
|
||||
"modified" => $currentTime,
|
||||
"uploader" => $uploader
|
||||
];
|
||||
}
|
||||
|
||||
// Save the updated metadata.
|
||||
file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode(["success" => "File saved successfully"]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Error saving file"]);
|
||||
|
||||
99
share.php
Normal file
99
share.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
// share.php
|
||||
require_once 'config.php';
|
||||
|
||||
// Get token and password (if provided)
|
||||
$token = isset($_GET['token']) ? $_GET['token'] : '';
|
||||
$providedPass = isset($_GET['pass']) ? $_GET['pass'] : '';
|
||||
|
||||
if (empty($token)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Missing token."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load share links.
|
||||
$shareFile = META_DIR . "share_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "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 json_encode(["error" => "Share link not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$record = $shareLinks[$token];
|
||||
|
||||
// Check expiration.
|
||||
if (time() > $record['expires']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "This link has expired."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If a password is required and none is provided, show a simple form.
|
||||
if (!empty($record['password']) && empty($providedPass)) {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Enter Password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2>This file is protected by a password.</h2>
|
||||
<form method="get" action="share.php">
|
||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
|
||||
<label for="pass">Password:</label>
|
||||
<input type="password" name="pass" id="pass" required>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
// If a password was set, validate it.
|
||||
if (!empty($record['password'])) {
|
||||
if (!password_verify($providedPass, $record['password'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid password."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Build file path.
|
||||
$folder = trim($record['folder'], "/\\ ");
|
||||
$file = $record['file'];
|
||||
$filePath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!empty($folder) && strtolower($folder) !== 'root') {
|
||||
$filePath .= $folder . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
$filePath .= $file;
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "File not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve the file.
|
||||
$mimeType = mime_content_type($filePath);
|
||||
header("Content-Type: " . $mimeType);
|
||||
|
||||
// Determine extension and set disposition accordingly.
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
|
||||
} else {
|
||||
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
|
||||
}
|
||||
|
||||
readfile($filePath);
|
||||
exit;
|
||||
?>
|
||||
80
styles.css
80
styles.css
@@ -1033,6 +1033,18 @@ body.dark-mode #fileListContainer {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.file-list-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-list-actions .action-btn {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
@@ -1089,7 +1101,7 @@ body.dark-mode #fileListContainer {
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1164,6 +1176,16 @@ body.dark-mode .image-preview-modal-content {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
/* Your custom styles here */
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
margin-left: 0px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.image-modal-img {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
@@ -1200,6 +1222,12 @@ body.dark-mode .image-preview-modal-content {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.share-modal-content {
|
||||
width: 600px !important;
|
||||
max-width: 90vw !important;
|
||||
/* ensures it doesn't exceed the viewport width */
|
||||
}
|
||||
|
||||
body.dark-mode .close-image-modal {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: #ff6666;
|
||||
@@ -1385,6 +1413,26 @@ body.dark-mode .btn-secondary {
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
#toggleViewBtn {
|
||||
margin-bottom: 20px;
|
||||
margin-left: 15px;
|
||||
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);
|
||||
/* More subtle drop shadow */
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#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;
|
||||
@@ -1552,4 +1600,34 @@ body.dark-mode .CodeMirror-selected {
|
||||
body.dark-mode .CodeMirror-matchingbracket {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
border-bottom: 1px solid #ffffff !important;
|
||||
}
|
||||
|
||||
.gallery-nav-btn {
|
||||
background: rgba(80, 80, 80, 0.6) !important;
|
||||
/* More translucent dark grey */
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
font-size: 48px !important;
|
||||
cursor: pointer !important;
|
||||
padding: 10px 15px !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;
|
||||
}
|
||||
|
||||
.gallery-nav-btn:hover {
|
||||
background: rgba(80, 80, 80, 0.8) !important;
|
||||
/* Slightly less translucent on hover */
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
/* If you need distinct positioning for left and right buttons */
|
||||
.gallery-nav-btn.left {
|
||||
left: 10px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.gallery-nav-btn.right {
|
||||
right: 10px;
|
||||
left: auto;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
require 'config.php'; // Must call session_start() and generate CSRF token if not set
|
||||
require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["csrf_token" => $_SESSION['csrf_token']]);
|
||||
echo json_encode([
|
||||
"csrf_token" => $_SESSION['csrf_token'],
|
||||
"share_url" => SHARE_URL
|
||||
]);
|
||||
?>
|
||||
83
upload.php
83
upload.php
@@ -3,7 +3,6 @@ require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection for Uploads ---
|
||||
// Use getallheaders() to read the token from the header.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
@@ -27,23 +26,22 @@ if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine the target upload directory.
|
||||
$uploadDir = UPLOAD_DIR;
|
||||
// Determine the base upload directory.
|
||||
$baseUploadDir = UPLOAD_DIR;
|
||||
if ($folder !== 'root') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($baseUploadDir)) {
|
||||
mkdir($baseUploadDir, 0775, true);
|
||||
}
|
||||
} else {
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
if (!is_dir($baseUploadDir)) {
|
||||
mkdir($baseUploadDir, 0775, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Load metadata for uploaded files.
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
|
||||
$metadataChanged = false;
|
||||
// Prepare a collection to hold metadata for each folder.
|
||||
$metadataCollection = []; // key: folder path, value: metadata array
|
||||
$metadataChanged = []; // key: folder path, value: boolean
|
||||
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
|
||||
@@ -63,38 +61,58 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
$relativePath = $_POST['relativePath'];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the complete folder path for upload and for metadata.
|
||||
$folderPath = $folder; // Base folder as provided ("root" or a subfolder)
|
||||
$uploadDir = $baseUploadDir; // Start with the base upload directory
|
||||
if (!empty($relativePath)) {
|
||||
$subDir = dirname($relativePath);
|
||||
if ($subDir !== '.' && $subDir !== '') {
|
||||
if ($folder === 'root') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
$safeFileName = basename($relativePath);
|
||||
// If base folder is 'root', then folderPath is just the subDir
|
||||
// Otherwise, append the subdirectory to the base folder
|
||||
$folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir;
|
||||
// Update the upload directory accordingly.
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
// Ensure the file name is taken from the relative path.
|
||||
$safeFileName = basename($relativePath);
|
||||
}
|
||||
// --- End Minimal Folder/Subfolder Logic ---
|
||||
|
||||
// Make sure the final upload directory exists.
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
|
||||
$targetPath = $uploadDir . $safeFileName;
|
||||
|
||||
if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) {
|
||||
if (!empty($relativePath)) {
|
||||
$metaKey = ($folder !== 'root') ? $folder . "/" . $relativePath : $relativePath;
|
||||
} else {
|
||||
$metaKey = ($folder !== 'root') ? $folder . "/" . $safeFileName : $safeFileName;
|
||||
// Generate a unique metadata file name based on the folder path.
|
||||
// Replace slashes, backslashes, and spaces with dashes.
|
||||
$metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath;
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
$metadataFile = META_DIR . $metadataFileName;
|
||||
|
||||
// Load metadata for this folder if not already loaded.
|
||||
if (!isset($metadataCollection[$metadataKey])) {
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadataCollection[$metadataKey] = json_decode(file_get_contents($metadataFile), true);
|
||||
} else {
|
||||
$metadataCollection[$metadataKey] = [];
|
||||
}
|
||||
$metadataChanged[$metadataKey] = false;
|
||||
}
|
||||
if (!isset($metadata[$metaKey])) {
|
||||
|
||||
// Add metadata for this file if not already present.
|
||||
if (!isset($metadataCollection[$metadataKey][$safeFileName])) {
|
||||
$uploadedDate = date(DATE_TIME_FORMAT);
|
||||
$uploader = $_SESSION['username'] ?? "Unknown";
|
||||
$metadata[$metaKey] = [
|
||||
$metadataCollection[$metadataKey][$safeFileName] = [
|
||||
"uploaded" => $uploadedDate,
|
||||
"uploader" => $uploader
|
||||
];
|
||||
$metadataChanged = true;
|
||||
$metadataChanged[$metadataKey] = true;
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["error" => "Error uploading file"]);
|
||||
@@ -102,8 +120,13 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($metadataChanged) {
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
// After processing all files, write out metadata files for folders that changed.
|
||||
foreach ($metadataCollection as $folderKey => $data) {
|
||||
if ($metadataChanged[$folderKey]) {
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json';
|
||||
$metadataFile = META_DIR . $metadataFileName;
|
||||
file_put_contents($metadataFile, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "Files uploaded successfully"]);
|
||||
|
||||
1
uploads/.htaccess
Normal file
1
uploads/.htaccess
Normal file
@@ -0,0 +1 @@
|
||||
Options -Indexes
|
||||
Reference in New Issue
Block a user