Folder sharing added

This commit is contained in:
Ryan
2025-04-09 01:46:07 -04:00
committed by GitHub
parent b9ce3f92a4
commit b2ff3efb3b
13 changed files with 993 additions and 91 deletions

View File

@@ -1,5 +1,43 @@
# Changelog
## Folder Sharing Feature - Changelog 4/9/2025
### New Endpoints
- **createFolderShareLink.php:**
- Generates secure, expiring share tokens for folders (with an optional password and allow-upload flag).
- Stores folder share records separately from file shares in `share_folder_links.json`.
- Builds share links that point to **shareFolder.php**, using a proper BASE_URL or the servers IP when a default placeholder is detected.
- **shareFolder.php:**
- Serves shared folders via GET requests by reading tokens from `share_folder_links.json`.
- Validates token expiration and password (if set).
- Displays folder contents with pagination (10 items per page) and shows file sizes in megabytes.
- Provides navigation links (Prev, Next, and numbered pages) for folder listings.
- Includes an upload form (if allowed) that redirects back to the same share page after upload.
- **downloadSharedFile.php:**
- A dedicated, secure download endpoint for shared files.
- Validates the share token and ensures the requested file is inside the shared folder.
- Serves files using proper MIME types and Content-Disposition headers (inline for images, attachment for others).
- **uploadToSharedFolder.php:**
- Handles file uploads for public folder shares.
- Enforces file size limits and file type whitelists.
- Generates unique filenames (with a unique prefix) to prevent collisions.
- Updates metadata for the uploaded file (upload date and sets uploader as "Outside Share").
- Redirects back to **shareFolder.php** after a successful upload so the file listing refreshes.
### New Front-End Module
- **folderShareModal.js:**
- Provides a modal interface for users to generate folder share links.
- Includes expiration selection, optional password entry, and an allow-upload checkbox.
- Uses the **createFolderShareLink.php** endpoint to generate share links.
- Displays the generated share link with a “copy to clipboard” button.
---
## Changes 4/8/2025
**May have missed some stuff or could have bugs. Please report any issue you may encounter.**
@@ -28,6 +66,9 @@
- Modal Popup for Single File: Replaced the direct download link for single files with a modal-driven flow. When the download button is clicked, the openDownloadModal(fileName, folder) function is called. This stores the file details and shows a modal where the user can confirm (or edit) the file name.
- Confirm Download Function: When the user clicks the Download button in the modal, the confirmSingleDownload() function is called. This function constructs a URL for download.php (using GET parameters for folder and file), fetches the file as a blob, and triggers a download using a temporary anchor element. A progress modal is also used here to give feedback during the download process.
- **Zip Extraction**
- Reused Zip Download modal to use same progress Modal Popup with Extracting files.... text.
---
## Changes 4/7/2025 v1.0.9

View File

@@ -12,17 +12,19 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
---
## Features at a Glance
## Features at a Glance or [Full Feature Wiki](https://github.com/error311/FileRise/wiki/Features)
- 🚀 **Easy File Uploads:** Upload multiple files and folders via drag & drop or file picker. Supports large files with pause/resumable chunked uploads and shows real-time progress for each file. No more failed transfers FileRise will pick up where it left off if your connection drops.
- 🗂️ **File Management:** Full set of file/folder operations move or copy files (via intuitive drag-drop or dialogs), rename items, and delete in batches. You can even download selected files as a ZIP archive or extract uploaded ZIP files server-side. Organize content with an interactive folder tree and breadcrumb navigation for quick jumps.
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) convenient for sending files without exposing the whole app.
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes tweak and save changes without leaving FileRise.
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags (labels) and later find them easily. The global search bar filters by filename or tag, making it simple to locate that “important” document in seconds. Tag management is built-in create, reuse, or remove tags as needed.
- 🔒 **User Authentication, User Permissions & Sharing:** Secure your portal with username/password login. Supports multiple users create user accounts (admin UI provided) for family or team members. User permissions such as User “Folder Only” feature assigns each user a dedicated folder within the root directory, named after their username, restricting them from viewing or modifying other directories. User Read Only and Disable Upload are additional permissions. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security. Share files with others using one-time or expiring public links (with password protection if desired) convenient for sending files without exposing the whole app.
- 🔒 **User Authentication & User Permissions:** Secure your portal with username/password login. Supports multiple users create user accounts (admin UI provided) for family or team members. User permissions such as User “Folder Only” feature assigns each user a dedicated folder within the root directory, named after their username, restricting them from viewing or modifying other directories. User Read Only and Disable Upload are additional permissions. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security.
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.

84
createFolderShareLink.php Normal file
View File

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

View File

@@ -849,7 +849,8 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
color: white;
}
.rename-btn .material-icons {
.rename-btn .material-icons,
#renameFolderBtn .material-icons {
color: black !important;
}

89
downloadSharedFile.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
// downloadSharedFile.php
require_once 'config.php';
// Retrieve and sanitize token and file name from GET.
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
$file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING);
if (empty($token) || empty($file)) {
http_response_code(400);
echo "Missing token or file parameter.";
exit;
}
// Load the share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$shareLinks = json_decode(file_get_contents($shareFile), true);
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$record = $shareLinks[$token];
// Check if the link has expired.
if (time() > $record['expires']) {
http_response_code(403);
echo "This share link has expired.";
exit;
}
// Get the shared folder from the record.
$folder = trim($record['folder'], "/\\ ");
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realFolderPath = realpath($folderPath);
$uploadDirReal = realpath(UPLOAD_DIR);
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
http_response_code(404);
echo "Shared folder not found.";
exit;
}
// Sanitize the filename to prevent directory traversal.
if (strpos($file, "/") !== false || strpos($file, "\\") !== false) {
http_response_code(400);
echo "Invalid file name.";
exit;
}
$file = basename($file);
// Build the full file path and verify it is inside the shared folder.
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
http_response_code(404);
echo "File not found.";
exit;
}
// Determine MIME type.
$mimeType = mime_content_type($realFilePath);
header("Content-Type: " . $mimeType);
// Set Content-Disposition header.
// Inline if the file is an image; attachment for others.
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
} else {
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
}
// Disable caching.
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
// Read and output the file.
readfile($realFilePath);
exit;
?>

View File

@@ -23,12 +23,24 @@
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js" integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js" integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js" integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js" integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js" integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js" integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"
integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"
integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"
integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js"
integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="css/styles.css" />
</head>
@@ -106,44 +118,46 @@
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons">
<button id="logoutBtn" data-i18n-title="logout">
<i class="material-icons">exit_to_app</i>
</button>
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
<i class="material-icons">vpn_key</i>
</button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i>
<span data-i18n-key="restore_text">Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span data-i18n-key="delete_text">Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete Selected</button>
<button id="deleteAllBtn" class="btn btn-danger" data-i18n-key="delete_all">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark" data-i18n-key="close">Close</button>
<div class="header-buttons">
<button id="logoutBtn" data-i18n-title="logout">
<i class="material-icons">exit_to_app</i>
</button>
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
<i class="material-icons">vpn_key</i>
</button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i>
<span data-i18n-key="restore_text">Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span data-i18n-key="delete_text">Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore
Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete
Selected</button>
<button id="deleteAllBtn" class="btn btn-danger" data-i18n-key="delete_all">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark" data-i18n-key="close">Close</button>
</div>
</div>
</div>
<button id="addUserBtn" data-i18n-title="add_user" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
</div>
<button id="addUserBtn" data-i18n-title="add_user" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
</div>
</div>
</div>
</header>
<!-- Custom Toast Container -->
@@ -181,7 +195,8 @@
</div>
<!-- Basic HTTP Login Option -->
<div class="text-center mt-3">
<a href="login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP Login</a>
<a href="login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
Login</a>
</div>
</div>
</div>
@@ -200,14 +215,16 @@
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
<span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose Files'</span>
<span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose
Files'</span>
<br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple
style="opacity:0; position:absolute; width:1px; height:1px;" />
<button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto" data-i18n-key="upload">Upload</button>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto"
data-i18n-key="upload">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
</div>
@@ -228,41 +245,56 @@
<div id="folderTreeContainer"></div>
</div>
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary" data-i18n-key="create_folder">Create Folder</button>
<button id="createFolderBtn" class="btn btn-primary" data-i18n-key="create_folder">Create
Folder</button>
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="create_folder_title">Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" data-i18n-placeholder="enter_folder_name" placeholder="Enter folder name"
<input type="text" id="newFolderName" class="form-control"
data-i18n-placeholder="enter_folder_name" placeholder="Enter folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary" data-i18n-key="create">Create</button>
<button id="cancelCreateFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary"
data-i18n-key="create">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="rename_folder">
<button id="renameFolderBtn" class="btn btn-warning ml-2" data-i18n-title="rename_folder">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="rename_folder_title">Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control" data-i18n-placeholder="rename_folder_placeholder" placeholder="Enter new folder name" style="margin-top:10px;" />
<input type="text" id="newRenameFolderName" class="form-control"
data-i18n-placeholder="rename_folder_placeholder" placeholder="Enter new folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary" data-i18n-key="rename">Rename</button>
<button id="cancelRenameFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary"
data-i18n-key="rename">Rename</button>
</div>
</div>
</div>
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
<i class="material-icons">share</i>
</button>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
<i class="material-icons">delete</i>
</button>
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="delete_folder_title">Delete Folder</h4>
<p id="deleteFolderMessage" data-i18n-key="delete_folder_message">Are you sure you want to delete this folder?</p>
<p id="deleteFolderMessage" data-i18n-key="delete_folder_message">Are you sure you want to
delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger" data-i18n-key="delete">Delete</button>
<button id="cancelDeleteFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger"
data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
@@ -272,8 +304,10 @@
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li data-i18n-key="folder_help_item_1">Click on a folder in the tree to view its files.</li>
<li data-i18n-key="folder_help_item_2">Use [-] to collapse and [+] to expand folders.</li>
<li data-i18n-key="folder_help_item_3">Select a folder and click "Create Folder" to add a subfolder.</li>
<li data-i18n-key="folder_help_item_4">To rename or delete a folder, select it and then click the appropriate button.</li>
<li data-i18n-key="folder_help_item_3">Select a folder and click "Create Folder" to add a
subfolder.</li>
<li data-i18n-key="folder_help_item_4">To rename or delete a folder, select it and then click
the appropriate button.</li>
</ul>
</div>
</div>
@@ -287,22 +321,26 @@
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;" data-i18n-key="delete_files">Delete Files</button>
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;"
data-i18n-key="delete_files">Delete Files</button>
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="delete_selected_files_title">Delete Selected Files</h4>
<p id="deleteFilesMessage" data-i18n-key="delete_files_message">Are you sure you want to delete the selected files?</p>
<p id="deleteFilesMessage" data-i18n-key="delete_files_message">Are you sure you want to delete the
selected files?</p>
<div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger" data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="copy_files">Copy Files</button>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="copy_files">Copy Files</button>
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="copy_files_title">Copy Selected Files</h4>
<p id="copyFilesMessage" data-i18n-key="copy_files_message">Select a target folder for copying the selected files:</p>
<p id="copyFilesMessage" data-i18n-key="copy_files_message">Select a target folder for copying the
selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
@@ -310,11 +348,13 @@
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="move_files">Move Files</button>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="move_files">Move Files</button>
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="move_files_title">Move Selected Files</h4>
<p id="moveFilesMessage" data-i18n-key="move_files_message">Select a target folder for moving the selected files:</p>
<p id="moveFilesMessage" data-i18n-key="move_files_message">Select a target folder for moving the
selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
@@ -322,13 +362,16 @@
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip" data-i18n-key="extract_zip_button">Extract Zip</button>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip"
data-i18n-key="extract_zip_button">Extract Zip</button>
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
<p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder" placeholder="files.zip" />
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder"
placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary" data-i18n-key="download">Download</button>
@@ -352,26 +395,31 @@
</div>
<!-- Single File Download Modal -->
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4>Download File</h4>
<p>Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary" onclick="document.getElementById('downloadFileModal').style.display = 'none';">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary" onclick="confirmSingleDownload()">Download</button>
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4>Download File</h4>
<p>Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary"
onclick="document.getElementById('downloadFileModal').style.display = 'none';">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary"
onclick="confirmSingleDownload()">Download</button>
</div>
</div>
</div>
</div>
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
<div id="changePasswordModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:400px; margin:auto;">
<span id="closeChangePasswordModal" style="cursor:pointer;">&times;</span>
<h3 data-i18n-key="change_password_title">Change Password</h3>
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password" placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" class="form-control" data-i18n-placeholder="new_password" placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" class="form-control" data-i18n-placeholder="confirm_new_password" placeholder="Confirm New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" class="form-control" data-i18n-placeholder="new_password"
placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" class="form-control" data-i18n-placeholder="confirm_new_password"
placeholder="Confirm New Password" style="width:100%; margin: 5px 0;" />
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
</div>
</div>
@@ -406,8 +454,8 @@
<div id="renameFileModal" class="modal">
<div class="modal-content">
<h4 data-i18n-key="rename_file_title">Rename File</h4>
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder" placeholder="Enter new file name"
style="margin-top:10px;" />
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
placeholder="Enter new file name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary" data-i18n-key="rename">Rename</button>

View File

@@ -2,7 +2,7 @@ import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.
import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js';
const version = "v1.0.9";
const version = "v1.1.0";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null;

View File

@@ -3,6 +3,7 @@
import { loadFileList } from './fileListView.js';
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
import { t } from './i18n.js';
import { openFolderShareModal } from './folderShareModal.js';
/* ----------------------
Helper Functions (Data/State)
@@ -418,7 +419,7 @@ export async function loadFolderTree(selectedFolder) {
localStorage.setItem("lastOpenedFolder", window.currentFolder);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
setupBreadcrumbDelegation();
loadFileList(window.currentFolder);
@@ -442,7 +443,7 @@ export async function loadFolderTree(selectedFolder) {
window.currentFolder = selected;
localStorage.setItem("lastOpenedFolder", selected);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
setupBreadcrumbDelegation();
loadFileList(selected);
});
@@ -735,18 +736,22 @@ function folderManagerContextMenuHandler(e) {
target.classList.add("selected");
const menuItems = [
{
label: "Create Folder",
label: t("create_folder"),
action: () => {
document.getElementById("createFolderModal").style.display = "block";
document.getElementById("newFolderName").focus();
}
},
{
label: "Rename Folder",
label: t("rename_folder"),
action: () => { openRenameFolderModal(); }
},
{
label: "Delete Folder",
label: t("folder_share"),
action: () => { openFolderShareModal(); }
},
{
label: t("delete_folder"),
action: () => { openDeleteFolderModal(); }
}
];
@@ -785,4 +790,21 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
document.addEventListener("DOMContentLoaded", function () {
const shareFolderBtn = document.getElementById("shareFolderBtn");
if (shareFolderBtn) {
shareFolderBtn.addEventListener("click", () => {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to share.");
return;
}
// Call the folder share modal from the module.
openFolderShareModal(selectedFolder);
});
} else {
console.warn("shareFolderBtn element not found in the DOM.");
}
});
bindFolderManagerContextMenu();

107
js/folderShareModal.js Normal file
View File

@@ -0,0 +1,107 @@
// folderShareModal.js
import { escapeHTML, showToast } from './domUtils.js';
import { t } from './i18n.js';
export function openFolderShareModal(folder) {
// Remove any existing folder share modal
const existing = document.getElementById("folderShareModal");
if (existing) existing.remove();
// Create the modal container
const modal = document.createElement("div");
modal.id = "folderShareModal";
modal.classList.add("modal");
modal.innerHTML = `
<div class="modal-content share-modal-content" style="width: 600px; max-width: 90vw;">
<div class="modal-header">
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
<span class="close-image-modal" id="closeFolderShareModal" title="Close">&times;</span>
</div>
<div class="modal-body">
<p>${t("set_expiration")}</p>
<select id="folderShareExpiration">
<option value="30">30 ${t("minutes")}</option>
<option value="60" selected>60 ${t("minutes")}</option>
<option value="120">120 ${t("minutes")}</option>
<option value="180">180 ${t("minutes")}</option>
<option value="240">240 ${t("minutes")}</option>
<option value="1440">1 ${t("day")}</option>
</select>
<p>${t("password_optional")}</p>
<input type="text" id="folderSharePassword" placeholder="${t("password")}" style="width: 100%;"/>
<br>
<label>
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
</label>
<br><br>
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
<p>${t("shareable_link")}</p>
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = "block";
// Close button handler
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
modal.remove();
});
// Handler for generating the share link
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
const expiration = document.getElementById("folderShareExpiration").value;
const password = document.getElementById("folderSharePassword").value;
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
// Retrieve the CSRF token from the meta tag.
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
if (!csrfToken) {
showToast(t("csrf_error"));
return;
}
// Post to the createFolderShareLink endpoint.
fetch("/createFolderShareLink.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
folder: folder,
expirationMinutes: parseInt(expiration, 10),
password: password,
allowUpload: allowUpload
})
})
.then(response => response.json())
.then(data => {
if (data.token && data.link) {
const shareUrl = data.link;
const displayDiv = document.getElementById("folderShareLinkDisplay");
const inputField = document.getElementById("folderShareLinkInput");
inputField.value = shareUrl;
displayDiv.style.display = "block";
showToast(t("share_link_generated"));
} else {
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
}
})
.catch(err => {
console.error("Error generating folder share link:", err);
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
});
});
// Copy share link button handler
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
const input = document.getElementById("folderShareLinkInput");
input.select();
document.execCommand("copy");
showToast(t("link_copied"));
});
}

View File

@@ -84,6 +84,7 @@ const translations = {
// File List keys:
"file_list_title": "Files in (Root)",
"files_in": "Files in",
"delete_files": "Delete Files",
"delete_selected_files_title": "Delete Selected Files",
"delete_files_message": "Are you sure you want to delete the selected files?",
@@ -126,6 +127,18 @@ const translations = {
// Rename File keys:
"rename_file_title": "Rename File",
"rename_file_placeholder": "Enter new file name",
// Folder Share
"share_folder": "Share Folder",
"allow_uploads": "Allow Uploads",
"share_link_generated": "Share Link Generated",
"error_generating_share_link": "Error Generating Share Link",
// Folder
"create_folder": "Create Folder",
"rename_folder": "Rename Folder",
"folder_share": "Share Folder",
"delete_folder": "Delete Folder",
// Custom Confirm Modal keys:
"yes": "Yes",
@@ -136,7 +149,14 @@ const translations = {
"copy": "Copy",
"extract": "Extract",
"user": "User:",
"password": "Password:",
"unknown_error": "Unknown Error",
"link_copied": "Link Copied to Clipboard",
"minutes": "minutes",
"hours": "hours",
"days": "days",
"weeks": "weeks",
"months": "months",
"seconds": "seconds",
// Dark Mode Toggle
"dark_mode_toggle": "Dark Mode",

View File

@@ -48,9 +48,6 @@ function getFilesFromDataTransferItems(items) {
return Promise.all(promises).then(results => results.flat());
}
/* -----------------------------------------------------
UI Helpers (Mostly unchanged from your original code)
----------------------------------------------------- */
function setDropAreaDefault() {
const dropArea = document.getElementById("uploadDropArea");
if (dropArea) {

338
shareFolder.php Normal file
View File

@@ -0,0 +1,338 @@
<?php
// shareFolder.php
require_once 'config.php';
// Retrieve token and optional password from GET.
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
$providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
if ($page === false || $page < 1) {
$page = 1;
}
if (empty($token)) {
http_response_code(400);
echo json_encode(["error" => "Missing token."]);
exit;
}
// Load share folder records.
$shareFile = META_DIR . "share_folder_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 password protection is enabled and no password is provided, show password form.
if (!empty($record['password']) && empty($providedPass)) {
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Enter Password</title>
<style>
body {
background-color: #f7f7f7;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 400px;
margin: 80px auto;
background: #fff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
font-size: 1.5rem;
text-align: center;
color: #333;
}
label, input, button {
display: block;
width: 100%;
}
input[type="password"] {
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007BFF;
border: none;
color: #fff;
padding: 10px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Folder Protected</h2>
<p>This folder is protected by a password.</p>
<form method="get" action="shareFolder.php">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<label for="pass">Password:</label>
<input type="password" name="pass" id="pass" required>
<button type="submit">Submit</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// Validate the provided password if required.
if (!empty($record['password'])) {
if (!password_verify($providedPass, $record['password'])) {
http_response_code(403);
echo json_encode(["error" => "Invalid password."]);
exit;
}
}
// Determine the folder path.
$folder = trim($record['folder'], "/\\ ");
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realFolderPath = realpath($folderPath);
$uploadDirReal = realpath(UPLOAD_DIR);
// Validate that the folder exists and is within UPLOAD_DIR.
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
http_response_code(404);
echo json_encode(["error" => "Folder not found."]);
exit;
}
// Scan and sort files.
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
}));
sort($allFiles);
// Pagination variables.
$itemsPerPage = 10;
$totalFiles = count($allFiles);
$totalPages = max(1, ceil($totalFiles / $itemsPerPage));
$currentPage = min($page, $totalPages);
$startIndex = ($currentPage - 1) * $itemsPerPage;
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background: #f2f2f2;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
color: #333;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
margin: 0;
font-size: 2rem;
}
.container {
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
border-bottom: 1px solid #ddd;
text-align: left;
}
th {
background: #007BFF;
color: #fff;
font-weight: normal;
}
tr:hover {
background: #f9f9f9;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Simple download icon style */
.download-icon {
margin-left: 8px;
font-weight: bold;
color: #007BFF;
}
/* Pagination styles */
.pagination {
text-align: center;
margin-top: 20px;
}
.pagination a, .pagination span {
margin: 0 5px;
padding: 8px 12px;
text-decoration: none;
background: #007BFF;
color: #fff;
border-radius: 4px;
}
.pagination span.current {
background: #0056b3;
}
.upload-container {
margin-top: 30px;
text-align: center;
}
.upload-container h3 {
font-size: 1.4rem;
margin-bottom: 10px;
}
.upload-container form {
display: inline-block;
margin-top: 10px;
}
.upload-container button {
background-color: #28a745;
border: none;
color: #fff;
padding: 10px 20px;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
}
.upload-container button:hover {
background-color: #218838;
}
.footer {
text-align: center;
margin-top: 40px;
font-size: 0.9rem;
color: #777;
}
</style>
</head>
<body>
<div class="header">
<h1>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></h1>
</div>
<div class="container">
<?php if (empty($filesOnPage)): ?>
<p style="text-align:center;">This folder is empty.</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>Filename</th>
<th>Size (MB)</th>
</tr>
</thead>
<tbody>
<?php foreach ($filesOnPage as $file):
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$sizeMB = round(filesize($filePath) / (1024 * 1024), 2);
// Build download link using share token and file name.
$downloadLink = "downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file);
?>
<tr>
<td>
<a href="<?php echo htmlspecialchars($downloadLink, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($file, ENT_QUOTES, 'UTF-8'); ?>
<span class="download-icon">&#x21E9;</span>
</a>
</td>
<td><?php echo $sizeMB; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Pagination Controls -->
<div class="pagination">
<?php if ($currentPage > 1): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage - 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Prev</a>
<?php else: ?>
<span>Prev</span>
<?php endif; ?>
<?php
// Display up to 5 page links centered around the current page.
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
for ($i = $startPage; $i <= $endPage; $i++): ?>
<?php if ($i == $currentPage): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $i; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage + 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Next</a>
<?php else: ?>
<span>Next</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($record['allowUpload']) : ?>
<div class="upload-container">
<h3>Upload File (50mb max size)</h3>
<form action="uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
<!-- Passing token so the upload endpoint can verify the share link. -->
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="file" name="fileToUpload" required>
<br><br>
<button type="submit">Upload</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="footer">
&copy; <?php echo date("Y"); ?> FileRise. All rights reserved.
</div>
</body>
</html>

153
uploadToSharedFolder.php Normal file
View File

@@ -0,0 +1,153 @@
<?php
// uploadToSharedFolder.php
require_once 'config.php';
// Only accept POST requests.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(["error" => "Method not allowed."]);
exit;
}
// Ensure the share token is provided.
if (empty($_POST['token'])) {
http_response_code(400);
echo json_encode(["error" => "Missing share token."]);
exit;
}
$token = trim($_POST['token']);
// Load the share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo json_encode(["error" => "Share record 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" => "Invalid share token."]);
exit;
}
$record = $shareLinks[$token];
// Check if the share link is expired.
if (time() > $record['expires']) {
http_response_code(403);
echo json_encode(["error" => "This share link has expired."]);
exit;
}
// Ensure that uploads are allowed for this share.
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
http_response_code(403);
echo json_encode(["error" => "File uploads are not allowed for this share."]);
exit;
}
// Check that a file was uploaded.
if (!isset($_FILES['fileToUpload'])) {
http_response_code(400);
echo json_encode(["error" => "No file was uploaded."]);
exit;
}
$fileUpload = $_FILES['fileToUpload'];
// Check for upload errors.
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(["error" => "File upload error. Code: " . $fileUpload['error']]);
exit;
}
// Enforce a maximum file size (e.g. 50MB).
$maxSize = 50 * 1024 * 1024; // 50MB
if ($fileUpload['size'] > $maxSize) {
http_response_code(400);
echo json_encode(["error" => "File size exceeds allowed limit."]);
exit;
}
// Define allowed file extensions.
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3'];
$uploadedName = basename($fileUpload['name']);
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) {
http_response_code(400);
echo json_encode(["error" => "File type not allowed."]);
exit;
}
// Determine the target folder from the share record.
$folder = trim($record['folder'], "/\\");
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realTargetFolder = realpath($targetFolder);
$uploadDirReal = realpath(UPLOAD_DIR);
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
http_response_code(404);
echo json_encode(["error" => "Shared folder not found."]);
exit;
}
// Generate a new filename to avoid collisions.
// A unique prefix (using uniqid) is prepended to help with uniqueness and traceability.
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
// Move the uploaded file securely.
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
http_response_code(500);
echo json_encode(["error" => "Failed to move the uploaded file."]);
exit;
}
// --- Metadata Update for Shared Upload ---
// We want to update metadata similarly to your normal upload.
// Determine a key for metadata storage for the folder.
$metadataKey = ($folder === '' || $folder === 'root') ? "root" : $folder;
// Sanitize the metadata file name.
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
// Load existing metadata if available.
$metadataCollection = [];
if (file_exists($metadataFile)) {
$data = file_get_contents($metadataFile);
$metadataCollection = json_decode($data, true);
if (!is_array($metadataCollection)) {
$metadataCollection = [];
}
}
// Set upload date using your defined format.
$uploadedDate = date(DATE_TIME_FORMAT);
// Since there is no logged-in user for public share uploads,
$uploader = "Outside Share";
// Update metadata for the new file.
if (!isset($metadataCollection[$newFilename])) {
$metadataCollection[$newFilename] = [
"uploaded" => $uploadedDate,
"uploader" => $uploader
];
}
// Save the metadata.
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
// --- End Metadata Update ---
// Optionally, set a flash message in session.
$_SESSION['upload_message'] = "File uploaded successfully.";
// Redirect back to the shared folder view, refreshing the file listing.
header("Location: shareFolder.php?token=" . urlencode($token));
exit;
?>