support custom expiration durations for file and folder shares (closes #26)
This commit is contained in:
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 4/28/2025
|
||||
|
||||
**Added**
|
||||
|
||||
- **Custom expiration** option to File Share modal
|
||||
- Users can specify a value + unit (seconds, minutes, hours, days)
|
||||
- Displays a warning when a custom duration is selected
|
||||
- **Custom expiration** option to Folder Share modal (same value+unit picker and warning)
|
||||
|
||||
**Changed**
|
||||
|
||||
- **API parameters** for both endpoints:
|
||||
- Replaced `expirationMinutes` with `expirationValue` + `expirationUnit`
|
||||
- Front-end now sends `{ expirationValue, expirationUnit }`
|
||||
- Back-end converts those into total seconds before saving
|
||||
- **UI**
|
||||
- FileShare and FolderShare modals updated to handle “Custom…” selection
|
||||
|
||||
**Updated Models & Controllers**
|
||||
|
||||
- **FileModel::createShareLink** now accepts expiration in seconds
|
||||
- **FolderModel::createShareFolderLink** now accepts expiration in seconds
|
||||
- **createShareLink.php** & **createShareFolderLink.php** updated to parse and convert new parameters
|
||||
|
||||
**Documentation**
|
||||
|
||||
- OpenAPI annotations for both endpoints updated to require `expirationValue` + `expirationUnit` (enum: seconds, minutes, hours, days)
|
||||
|
||||
## Changes 4/27/2025 1.2.7
|
||||
|
||||
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
|
||||
|
||||
@@ -4,36 +4,68 @@ import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("shareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Build the modal
|
||||
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-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||
<div class="modal-header">
|
||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</span>
|
||||
<span id="closeShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("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 id="shareExpiration" style="width:100%;padding:5px;">
|
||||
<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>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
||||
<br>
|
||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
||||
|
||||
<div id="customExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="sharePassword"
|
||||
placeholder="${t("password_optional")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<button
|
||||
id="generateShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="shareLinkDisplay" style="margin-top:15px;display:none;">
|
||||
<p>${t("shareable_link")}</p>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
|
||||
<input type="text" id="shareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,52 +73,72 @@ export function openShareModal(file, folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close handler
|
||||
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("/api/file/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
|
||||
// Show/hide custom-duration inputs
|
||||
document.getElementById("shareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
const container = document.getElementById("customExpirationContainer");
|
||||
container.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate share link
|
||||
document.getElementById("generateShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("shareExpiration");
|
||||
let value, unit;
|
||||
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customExpirationValue").value, 10);
|
||||
unit = document.getElementById("customExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("sharePassword").value;
|
||||
|
||||
fetch("/api/file/createShareLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
file: file.name,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.token) {
|
||||
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
|
||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
||||
const inputField = document.getElementById("shareLinkInput");
|
||||
inputField.value = shareUrl;
|
||||
displayDiv.style.display = "block";
|
||||
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
|
||||
document.getElementById("shareLinkInput").value = url;
|
||||
document.getElementById("shareLinkDisplay").style.display = "block";
|
||||
} else {
|
||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
||||
showToast(t("error_generating_share") + ": " + (data.error||"Unknown"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error generating share link:", err);
|
||||
showToast("Error generating share link.");
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast("Link copied to clipboard!");
|
||||
});
|
||||
// Copy to clipboard
|
||||
document.getElementById("copyShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const input = document.getElementById("shareLinkInput");
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
|
||||
@@ -1,44 +1,75 @@
|
||||
// folderShareModal.js
|
||||
// js/folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing folder share modal
|
||||
// Remove any existing modal
|
||||
const existing = document.getElementById("folderShareModal");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Create the modal container
|
||||
// Build modal
|
||||
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-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">×</span>
|
||||
<span id="closeFolderShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("set_expiration")}</p>
|
||||
<select id="folderShareExpiration">
|
||||
<select id="folderShareExpiration" style="width:100%;padding:5px;">
|
||||
<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>
|
||||
<option value="custom">${t("custom")}…</option>
|
||||
</select>
|
||||
<p>${t("password_optional")}</p>
|
||||
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
||||
|
||||
<div id="customFolderExpirationContainer" style="display:none;margin-top:10px;">
|
||||
<label for="customFolderExpirationValue">${t("duration")}:</label>
|
||||
<input type="number" id="customFolderExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||
<select id="customFolderExpirationUnit">
|
||||
<option value="seconds">${t("seconds")}</option>
|
||||
<option value="minutes" selected>${t("minutes")}</option>
|
||||
<option value="hours">${t("hours")}</option>
|
||||
<option value="days">${t("days")}</option>
|
||||
</select>
|
||||
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||
${t("custom_duration_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||
<input
|
||||
type="text"
|
||||
id="folderSharePassword"
|
||||
placeholder="${t("enter_password")}"
|
||||
style="width:100%;padding:5px;"
|
||||
/>
|
||||
|
||||
<label style="margin-top:10px;display:block;">
|
||||
<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;">
|
||||
|
||||
<button
|
||||
id="generateFolderShareLinkBtn"
|
||||
class="btn btn-primary"
|
||||
style="margin-top:15px;"
|
||||
>
|
||||
${t("generate_share_link")}
|
||||
</button>
|
||||
|
||||
<div id="folderShareLinkDisplay" style="margin-top:15px;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>
|
||||
<input type="text" id="folderShareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||
<button id="copyFolderShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||
${t("copy_link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,62 +77,75 @@ export function openFolderShareModal(folder) {
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
|
||||
// Close button handler
|
||||
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
// Close
|
||||
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("/api/folder/createShareFolderLink.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
|
||||
// Toggle custom inputs
|
||||
document.getElementById("folderShareExpiration")
|
||||
.addEventListener("change", e => {
|
||||
document.getElementById("customFolderExpirationContainer")
|
||||
.style.display = e.target.value === "custom" ? "block" : "none";
|
||||
});
|
||||
|
||||
// Generate link
|
||||
document.getElementById("generateFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const sel = document.getElementById("folderShareExpiration");
|
||||
let value, unit;
|
||||
if (sel.value === "custom") {
|
||||
value = parseInt(document.getElementById("customFolderExpirationValue").value, 10);
|
||||
unit = document.getElementById("customFolderExpirationUnit").value;
|
||||
} else {
|
||||
value = parseInt(sel.value, 10);
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
const password = document.getElementById("folderSharePassword").value;
|
||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||
if (!csrfToken) {
|
||||
showToast(t("csrf_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/folder/createShareFolderLink.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder,
|
||||
expirationValue: value,
|
||||
expirationUnit: unit,
|
||||
password,
|
||||
allowUpload
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(r => r.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";
|
||||
document.getElementById("folderShareLinkInput").value = data.link;
|
||||
document.getElementById("folderShareLinkDisplay").style.display = "block";
|
||||
showToast(t("share_link_generated"));
|
||||
} else {
|
||||
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
|
||||
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")));
|
||||
console.error(err);
|
||||
showToast(t("error_generating_share_link") + ": " + 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"));
|
||||
});
|
||||
// Copy
|
||||
document.getElementById("copyFolderShareLinkBtn")
|
||||
.addEventListener("click", () => {
|
||||
const inp = document.getElementById("folderShareLinkInput");
|
||||
inp.select();
|
||||
document.execCommand("copy");
|
||||
showToast(t("link_copied"));
|
||||
});
|
||||
}
|
||||
@@ -150,6 +150,13 @@ const translations = {
|
||||
"allow_uploads": "Allow Uploads",
|
||||
"share_link_generated": "Share Link Generated",
|
||||
"error_generating_share_link": "Error Generating Share Link",
|
||||
"custom": "Custom",
|
||||
"duration": "Duration",
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"custom_duration_warning": "⚠️ Using a long expiration may pose security risks. Use with caution.",
|
||||
|
||||
// Folder
|
||||
"folder_share": "Share Folder",
|
||||
@@ -166,12 +173,8 @@ const translations = {
|
||||
"user": "User:",
|
||||
"unknown_error": "Unknown Error",
|
||||
"link_copied": "Link Copied to Clipboard",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours",
|
||||
"days": "days",
|
||||
"weeks": "weeks",
|
||||
"months": "months",
|
||||
"seconds": "seconds",
|
||||
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dark Mode",
|
||||
@@ -239,7 +242,7 @@ const translations = {
|
||||
"ok": "OK",
|
||||
"show": "Show",
|
||||
"items_per_page": "items per page",
|
||||
"columns":"Columns",
|
||||
"columns": "Columns",
|
||||
"api_docs": "API Docs"
|
||||
},
|
||||
es: {
|
||||
@@ -806,7 +809,7 @@ const translations = {
|
||||
"prev": "Zurück",
|
||||
"next": "Weiter",
|
||||
"page": "Seite",
|
||||
"of": "von",
|
||||
"of": "von",
|
||||
|
||||
// Login Form keys:
|
||||
"login": "Anmelden",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/FileModel.php';
|
||||
|
||||
class FileController {
|
||||
class FileController
|
||||
{
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/copyFiles.php",
|
||||
@@ -50,9 +51,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function copyFiles() {
|
||||
public function copyFiles()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -61,14 +63,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check user permissions (assuming loadUserPermissions() is available).
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -76,7 +78,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to copy files."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get JSON input data.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
@@ -89,11 +91,11 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid request"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$sourceFolder = trim($data['source']);
|
||||
$destinationFolder = trim($data['destination']);
|
||||
$files = $data['files'];
|
||||
|
||||
|
||||
// Validate folder names.
|
||||
if ($sourceFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
@@ -103,7 +105,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid destination folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::copyFiles($sourceFolder, $destinationFolder, $files);
|
||||
echo json_encode($result);
|
||||
@@ -153,9 +155,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function deleteFiles() {
|
||||
public function deleteFiles()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -164,14 +167,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Load user's permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -179,7 +182,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to delete files."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!isset($data['files']) || !is_array($data['files'])) {
|
||||
@@ -187,7 +190,7 @@ class FileController {
|
||||
echo json_encode(["error" => "No file names provided"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Determine folder; default to 'root'.
|
||||
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
@@ -195,13 +198,13 @@ class FileController {
|
||||
exit;
|
||||
}
|
||||
$folder = trim($folder, "/\\ ");
|
||||
|
||||
|
||||
// Delegate to the FileModel.
|
||||
$result = FileModel::deleteFiles($folder, $data['files']);
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/moveFiles.php",
|
||||
* summary="Move files between folders",
|
||||
@@ -246,9 +249,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function moveFiles() {
|
||||
public function moveFiles()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -257,14 +261,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Verify that the user is not read-only.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -272,7 +276,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to move files."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (
|
||||
@@ -285,10 +289,10 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid request"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$sourceFolder = trim($data['source']) ?: 'root';
|
||||
$destinationFolder = trim($data['destination']) ?: 'root';
|
||||
|
||||
|
||||
// Validate folder names.
|
||||
if ($sourceFolder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
@@ -298,13 +302,13 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid destination folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::moveFiles($sourceFolder, $destinationFolder, $data['files']);
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/renameFile.php",
|
||||
* summary="Rename a file",
|
||||
@@ -346,12 +350,13 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs a JSON response.
|
||||
*/
|
||||
public function renameFile() {
|
||||
public function renameFile()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -360,14 +365,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Verify user permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -375,7 +380,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to rename files."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) {
|
||||
@@ -383,29 +388,29 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid input"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$folder = trim($data['folder']) ?: 'root';
|
||||
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$oldName = basename(trim($data['oldName']));
|
||||
$newName = basename(trim($data['newName']));
|
||||
|
||||
|
||||
// Validate file names.
|
||||
if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) {
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate the renaming operation to the model.
|
||||
$result = FileModel::renameFile($folder, $oldName, $newName);
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/saveFile.php",
|
||||
* summary="Save a file",
|
||||
@@ -446,9 +451,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs a JSON response.
|
||||
*/
|
||||
public function saveFile() {
|
||||
public function saveFile()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||
@@ -457,14 +463,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// --- Authentication Check ---
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
// --- Read‑only check ---
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -472,7 +478,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// --- Input parsing ---
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (empty($data) || !isset($data["fileName"], $data["content"])) {
|
||||
@@ -480,17 +486,17 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$fileName = basename($data["fileName"]);
|
||||
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
||||
|
||||
|
||||
// --- Folder validation ---
|
||||
if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
$folder = trim($folder, "/\\ ");
|
||||
|
||||
|
||||
// --- Delegate to model, passing the uploader ---
|
||||
// Make sure FileModel::saveFile signature is:
|
||||
// saveFile(string $folder, string $fileName, $content, ?string $uploader = null)
|
||||
@@ -500,7 +506,7 @@ class FileController {
|
||||
$data["content"],
|
||||
$username // ← pass the real uploader here
|
||||
);
|
||||
|
||||
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
@@ -555,7 +561,8 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs file content with appropriate headers.
|
||||
*/
|
||||
public function downloadFile() {
|
||||
public function downloadFile()
|
||||
{
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
@@ -563,31 +570,31 @@ class FileController {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get GET parameters.
|
||||
$file = isset($_GET['file']) ? basename($_GET['file']) : '';
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
|
||||
|
||||
// Validate the file name using REGEX_FILE_NAME.
|
||||
if (!preg_match(REGEX_FILE_NAME, $file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Retrieve download info from the model.
|
||||
$downloadInfo = FileModel::getDownloadInfo($folder, $file);
|
||||
if (isset($downloadInfo['error'])) {
|
||||
http_response_code( (in_array($downloadInfo['error'], ["File not found.", "Access forbidden."])) ? 404 : 400 );
|
||||
http_response_code((in_array($downloadInfo['error'], ["File not found.", "Access forbidden."])) ? 404 : 400);
|
||||
echo json_encode(["error" => $downloadInfo['error']]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Serve the file.
|
||||
$realFilePath = $downloadInfo['filePath'];
|
||||
$mimeType = $downloadInfo['mimeType'];
|
||||
header("Content-Type: " . $mimeType);
|
||||
|
||||
|
||||
// For images, serve inline; for others, force download.
|
||||
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||
$inlineImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
||||
@@ -601,7 +608,7 @@ class FileController {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/downloadZip.php",
|
||||
* summary="Download a ZIP archive of selected files",
|
||||
@@ -649,7 +656,8 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs the ZIP file for download.
|
||||
*/
|
||||
public function downloadZip() {
|
||||
public function downloadZip()
|
||||
{
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -659,7 +667,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
@@ -667,7 +675,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Read and decode JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
@@ -676,10 +684,10 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
|
||||
// Validate folder: if not "root", split and validate each segment.
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
@@ -692,7 +700,7 @@ class FileController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create ZIP archive using FileModel.
|
||||
$result = FileModel::createZipArchive($folder, $files);
|
||||
if (isset($result['error'])) {
|
||||
@@ -701,7 +709,7 @@ class FileController {
|
||||
echo json_encode(["error" => $result['error']]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$zipPath = $result['zipPath'];
|
||||
if (!file_exists($zipPath)) {
|
||||
http_response_code(500);
|
||||
@@ -709,21 +717,21 @@ class FileController {
|
||||
echo json_encode(["error" => "ZIP archive not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Send headers to force download.
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="files.zip"');
|
||||
header('Content-Length: ' . filesize($zipPath));
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
|
||||
// Output the ZIP file.
|
||||
readfile($zipPath);
|
||||
unlink($zipPath);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/extractZip.php",
|
||||
* summary="Extract ZIP files",
|
||||
@@ -768,9 +776,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function extractZip() {
|
||||
public function extractZip()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -779,14 +788,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Read and decode JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
@@ -794,10 +803,10 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
|
||||
// Validate folder name.
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', trim($folder));
|
||||
@@ -809,13 +818,13 @@ class FileController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::extractZipArchive($folder, $files);
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/share.php",
|
||||
* summary="Access a shared file",
|
||||
@@ -860,18 +869,19 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs either HTML (password form) or serves the file.
|
||||
*/
|
||||
public function shareFile() {
|
||||
public function shareFile()
|
||||
{
|
||||
// Retrieve and sanitize GET parameters.
|
||||
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
|
||||
$providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
|
||||
|
||||
|
||||
if (empty($token)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Missing token."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Get share record from the model.
|
||||
$record = FileModel::getShareRecord($token);
|
||||
if (!$record) {
|
||||
@@ -880,7 +890,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Share link not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check expiration.
|
||||
if (time() > $record['expires']) {
|
||||
http_response_code(403);
|
||||
@@ -888,13 +898,14 @@ class FileController {
|
||||
echo json_encode(["error" => "This link has expired."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// If a password is required and not provided, show an HTML form.
|
||||
if (!empty($record['password']) && empty($providedPass)) {
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
?>
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -906,14 +917,16 @@ class FileController {
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 400px;
|
||||
margin: 40px auto;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
@@ -921,6 +934,7 @@ class FileController {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #007BFF;
|
||||
@@ -929,11 +943,13 @@ class FileController {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>This file is protected by a password.</h2>
|
||||
<form method="get" action="/api/file/share.php">
|
||||
@@ -943,11 +959,12 @@ class FileController {
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<?php
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// If a password is required, validate the provided password.
|
||||
if (!empty($record['password'])) {
|
||||
if (!password_verify($providedPass, $record['password'])) {
|
||||
@@ -957,7 +974,7 @@ class FileController {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build file path securely.
|
||||
$folder = trim($record['folder'], "/\\ ");
|
||||
$file = $record['file'];
|
||||
@@ -966,7 +983,7 @@ class FileController {
|
||||
$filePath .= $folder . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
$filePath .= $file;
|
||||
|
||||
|
||||
$realFilePath = realpath($filePath);
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
|
||||
@@ -981,12 +998,12 @@ class FileController {
|
||||
echo json_encode(["error" => "File not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Serve the file.
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
header("Content-Type: " . $mimeType);
|
||||
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
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) . '"');
|
||||
@@ -994,25 +1011,31 @@ class FileController {
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header('Content-Length: ' . filesize($realFilePath));
|
||||
|
||||
|
||||
readfile($realFilePath);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/createShareLink.php",
|
||||
* summary="Create a share link for a file",
|
||||
* description="Generates a secure share link token for a specific file with an optional password protection and expiration time.",
|
||||
* description="Generates a secure share link token for a specific file with optional password protection and a custom expiration time.",
|
||||
* operationId="createShareLink",
|
||||
* tags={"Files"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"folder", "file"},
|
||||
* required={"folder", "file", "expirationValue", "expirationUnit"},
|
||||
* @OA\Property(property="folder", type="string", example="Documents"),
|
||||
* @OA\Property(property="file", type="string", example="report.pdf"),
|
||||
* @OA\Property(property="expirationMinutes", type="integer", example=60),
|
||||
* @OA\Property(property="expirationValue", type="integer", example=1),
|
||||
* @OA\Property(
|
||||
* property="expirationUnit",
|
||||
* type="string",
|
||||
* enum={"seconds","minutes","hours","days"},
|
||||
* example="minutes"
|
||||
* ),
|
||||
* @OA\Property(property="password", type="string", example="secret")
|
||||
* )
|
||||
* ),
|
||||
@@ -1042,25 +1065,26 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function createShareLink() {
|
||||
public function createShareLink()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check user permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
if ($username && !empty($userPermissions['readOnly'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create share links."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Parse POST JSON input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input) {
|
||||
@@ -1068,26 +1092,45 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Extract parameters.
|
||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||
$value = isset($input['expirationValue']) ? intval($input['expirationValue']) : 60;
|
||||
$unit = isset($input['expirationUnit']) ? $input['expirationUnit'] : 'minutes';
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
|
||||
// Validate folder.
|
||||
|
||||
// Validate folder name.
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Convert the provided value+unit into seconds
|
||||
switch ($unit) {
|
||||
case 'seconds':
|
||||
$expirationSeconds = $value;
|
||||
break;
|
||||
case 'hours':
|
||||
$expirationSeconds = $value * 3600;
|
||||
break;
|
||||
case 'days':
|
||||
$expirationSeconds = $value * 86400;
|
||||
break;
|
||||
case 'minutes':
|
||||
default:
|
||||
$expirationSeconds = $value * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
// Delegate share link creation to the model.
|
||||
$result = FileModel::createShareLink($folder, $file, $expirationMinutes, $password);
|
||||
$result = FileModel::createShareLink($folder, $file, $expirationSeconds, $password);
|
||||
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getTrashItems.php",
|
||||
* summary="Get trash items",
|
||||
@@ -1109,16 +1152,17 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response with trash items.
|
||||
*/
|
||||
public function getTrashItems() {
|
||||
public function getTrashItems()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$trashItems = FileModel::getTrashItems();
|
||||
echo json_encode($trashItems);
|
||||
@@ -1164,9 +1208,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function restoreFiles() {
|
||||
public function restoreFiles()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// CSRF Protection.
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -1175,14 +1220,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Read POST input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!isset($data['files']) || !is_array($data['files'])) {
|
||||
@@ -1190,13 +1235,13 @@ class FileController {
|
||||
echo json_encode(["error" => "No file or folder identifiers provided"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate restoration to the model.
|
||||
$result = FileModel::restoreFiles($data['files']);
|
||||
echo json_encode($result);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/deleteTrashFiles.php",
|
||||
* summary="Delete trash files",
|
||||
@@ -1247,9 +1292,10 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs a JSON response.
|
||||
*/
|
||||
public function deleteTrashFiles() {
|
||||
public function deleteTrashFiles()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// CSRF Protection.
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
@@ -1258,14 +1304,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Read and decode JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) {
|
||||
@@ -1273,7 +1319,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid input"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Determine deletion mode.
|
||||
$filesToDelete = [];
|
||||
if (isset($data['deleteAll']) && $data['deleteAll'] === true) {
|
||||
@@ -1299,14 +1345,14 @@ class FileController {
|
||||
echo json_encode(["error" => "No trash file identifiers provided"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate deletion to the model.
|
||||
$result = FileModel::deleteTrashFiles($filesToDelete);
|
||||
|
||||
// Build a human‑friendly success or error message
|
||||
if (!empty($result['deleted'])) {
|
||||
$count = count($result['deleted']);
|
||||
$msg = "Trash item" . ($count===1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
|
||||
$msg = "Trash item" . ($count === 1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
|
||||
echo json_encode(["success" => $msg]);
|
||||
} elseif (!empty($result['error'])) {
|
||||
echo json_encode(["error" => $result['error']]);
|
||||
@@ -1316,7 +1362,7 @@ class FileController {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/getFileTag.php",
|
||||
* summary="Retrieve file tags",
|
||||
@@ -1337,15 +1383,16 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response with file tags.
|
||||
*/
|
||||
public function getFileTags(): void {
|
||||
public function getFileTags(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
|
||||
$tags = FileModel::getFileTags();
|
||||
echo json_encode($tags);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/saveFileTag.php",
|
||||
* summary="Save file tags",
|
||||
@@ -1397,7 +1444,8 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function saveFileTag(): void {
|
||||
public function saveFileTag(): void
|
||||
{
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
@@ -1411,14 +1459,14 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check that the user is not read-only.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
@@ -1426,7 +1474,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to file tags"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Retrieve and sanitize input.
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$data) {
|
||||
@@ -1434,26 +1482,26 @@ class FileController {
|
||||
echo json_encode(["error" => "No data received"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$file = isset($data['file']) ? trim($data['file']) : '';
|
||||
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
||||
$tags = $data['tags'] ?? [];
|
||||
$deleteGlobal = isset($data['deleteGlobal']) ? (bool)$data['deleteGlobal'] : false;
|
||||
$tagToDelete = isset($data['tagToDelete']) ? trim($data['tagToDelete']) : null;
|
||||
|
||||
|
||||
if ($file === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "No file specified."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Validate folder name.
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::saveFileTag($folder, $file, $tags, $deleteGlobal, $tagToDelete);
|
||||
echo json_encode($result);
|
||||
@@ -1496,16 +1544,17 @@ class FileController {
|
||||
*
|
||||
* @return void Outputs JSON response.
|
||||
*/
|
||||
public function getFileList(): void {
|
||||
public function getFileList(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Retrieve the folder from GET; default to "root".
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
@@ -1513,7 +1562,7 @@ class FileController {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::getFileList($folder);
|
||||
if (isset($result['error'])) {
|
||||
@@ -1522,4 +1571,4 @@ class FileController {
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ class FolderController
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -847,38 +847,63 @@ class FolderController
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
// Auth check
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check that the user is not read-only.
|
||||
// Read-only check
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
$perms = loadUserPermissions($username);
|
||||
if ($username && !empty($perms['readOnly'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Retrieve and decode POST input.
|
||||
$input = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$input || !isset($input['folder'])) {
|
||||
// Input
|
||||
$in = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$in || !isset($in['folder'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = trim($input['folder']);
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
||||
$folder = trim($in['folder']);
|
||||
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||
$password = $in['password'] ?? '';
|
||||
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload);
|
||||
echo json_encode($result);
|
||||
// Folder name validation
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
switch ($unit) {
|
||||
case 'seconds':
|
||||
$seconds = $value;
|
||||
break;
|
||||
case 'hours':
|
||||
$seconds = $value * 3600;
|
||||
break;
|
||||
case 'days':
|
||||
$seconds = $value * 86400;
|
||||
break;
|
||||
case 'minutes':
|
||||
default:
|
||||
$seconds = $value * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
// Delegate
|
||||
$res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload);
|
||||
echo json_encode($res);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -736,7 +736,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
* @return array Returns an associative array with keys "token" and "expires" on success,
|
||||
* or "error" on failure.
|
||||
*/
|
||||
public static function createShareLink($folder, $file, $expirationMinutes = 60, $password = "") {
|
||||
public static function createShareLink($folder, $file, $expirationSeconds = 3600, $password = "") {
|
||||
// Validate folder if necessary (this can also be done in the controller).
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -746,7 +746,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
// Calculate expiration (Unix timestamp).
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class FolderModel {
|
||||
class FolderModel
|
||||
{
|
||||
/**
|
||||
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
||||
*
|
||||
@@ -12,10 +13,11 @@ class FolderModel {
|
||||
* @return array Returns an array with a "success" key if the folder was created,
|
||||
* or an "error" key if an error occurred.
|
||||
*/
|
||||
public static function createFolder(string $folderName, string $parent = ""): array {
|
||||
public static function createFolder(string $folderName, string $parent = ""): array
|
||||
{
|
||||
$folderName = trim($folderName);
|
||||
$parent = trim($parent);
|
||||
|
||||
|
||||
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -23,7 +25,7 @@ class FolderModel {
|
||||
if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||
return ["error" => "Invalid parent folder name."];
|
||||
}
|
||||
|
||||
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
if ($parent !== "" && strtolower($parent) !== "root") {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
|
||||
@@ -32,12 +34,12 @@ class FolderModel {
|
||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
|
||||
$relativePath = $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Check if the folder already exists.
|
||||
if (file_exists($fullPath)) {
|
||||
return ["error" => "Folder already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to create the folder.
|
||||
if (mkdir($fullPath, 0755, true)) {
|
||||
// Create an empty metadata file for the new folder.
|
||||
@@ -50,52 +52,54 @@ class FolderModel {
|
||||
return ["error" => "Failed to create folder."];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the metadata file path for a given folder.
|
||||
*
|
||||
* @param string $folder The relative folder path.
|
||||
* @return string The metadata file path.
|
||||
*/
|
||||
private static function getMetadataFilePath(string $folder): string {
|
||||
private static function getMetadataFilePath(string $folder): string
|
||||
{
|
||||
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Deletes a folder if it is empty and removes its corresponding metadata.
|
||||
*
|
||||
* @param string $folder The folder name (relative to the upload directory).
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function deleteFolder(string $folder): array {
|
||||
public static function deleteFolder(string $folder): array
|
||||
{
|
||||
// Prevent deletion of "root".
|
||||
if (strtolower($folder) === 'root') {
|
||||
return ["error" => "Cannot delete root folder."];
|
||||
}
|
||||
|
||||
|
||||
// Validate folder name.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder path.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder;
|
||||
|
||||
|
||||
// Check if the folder exists and is a directory.
|
||||
if (!file_exists($folderPath) || !is_dir($folderPath)) {
|
||||
return ["error" => "Folder does not exist."];
|
||||
}
|
||||
|
||||
|
||||
// Prevent deletion if the folder is not empty.
|
||||
$items = array_diff(scandir($folderPath), array('.', '..'));
|
||||
if (count($items) > 0) {
|
||||
return ["error" => "Folder is not empty."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to delete the folder.
|
||||
if (rmdir($folderPath)) {
|
||||
// Remove corresponding metadata file.
|
||||
@@ -109,43 +113,45 @@ class FolderModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Renames a folder and updates related metadata files.
|
||||
*
|
||||
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
||||
* @param string $newFolder The new folder name.
|
||||
* @return array Returns an associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array {
|
||||
public static function renameFolder(string $oldFolder, string $newFolder): array
|
||||
{
|
||||
// Sanitize and trim folder names.
|
||||
$oldFolder = trim($oldFolder, "/\\ ");
|
||||
$newFolder = trim($newFolder, "/\\ ");
|
||||
|
||||
|
||||
// Validate folder names.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||
return ["error" => "Invalid folder name(s)."];
|
||||
}
|
||||
|
||||
|
||||
// Build the full folder paths.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
||||
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
||||
|
||||
|
||||
// Validate that the old folder exists and new folder does not.
|
||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0
|
||||
) {
|
||||
return ["error" => "Invalid folder path."];
|
||||
}
|
||||
|
||||
|
||||
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
||||
return ["error" => "Folder to rename does not exist."];
|
||||
}
|
||||
|
||||
|
||||
if (file_exists($newPath)) {
|
||||
return ["error" => "New folder name already exists."];
|
||||
}
|
||||
|
||||
|
||||
// Attempt to rename the folder.
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata: Rename all metadata files that have the old folder prefix.
|
||||
@@ -171,7 +177,8 @@ class FolderModel {
|
||||
* @param string $relative The relative path from the base directory.
|
||||
* @return array An array of folder paths (relative to the base).
|
||||
*/
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array {
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array
|
||||
{
|
||||
$folders = [];
|
||||
$items = scandir($dir);
|
||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||
@@ -198,7 +205,8 @@ class FolderModel {
|
||||
*
|
||||
* @return array An array of folder information arrays.
|
||||
*/
|
||||
public static function getFolderList(): array {
|
||||
public static function getFolderList(): array
|
||||
{
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderInfoList = [];
|
||||
|
||||
@@ -240,13 +248,14 @@ class FolderModel {
|
||||
return $folderInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves the share folder record for a given token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
* @return array|null The share folder record, or null if not found.
|
||||
*/
|
||||
public static function getShareFolderRecord(string $token): ?array {
|
||||
public static function getShareFolderRecord(string $token): ?array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return null;
|
||||
@@ -257,8 +266,8 @@ class FolderModel {
|
||||
}
|
||||
return $shareLinks[$token];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieves shared folder data based on a share token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -274,7 +283,8 @@ class FolderModel {
|
||||
* - 'totalPages': total pages,
|
||||
* or an 'error' key on failure.
|
||||
*/
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array {
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -314,7 +324,7 @@ class FolderModel {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
// Scan for files (only files).
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) {
|
||||
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||
}));
|
||||
sort($allFiles);
|
||||
@@ -323,7 +333,7 @@ class FolderModel {
|
||||
$currentPage = min($page, $totalPages);
|
||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
|
||||
|
||||
return [
|
||||
"record" => $record,
|
||||
"folder" => $folder,
|
||||
@@ -334,81 +344,72 @@ class FolderModel {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a share link for a folder.
|
||||
*
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
||||
* @param string $password Optional password for the share.
|
||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationSeconds How many seconds until expiry.
|
||||
* @param string $password Optional password.
|
||||
* @param int $allowUpload 0 or 1 whether uploads are allowed.
|
||||
* @return array ["token","expires","link"] on success, or ["error"].
|
||||
*/
|
||||
public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
||||
// Validate folder name.
|
||||
public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array
|
||||
{
|
||||
// Validate folder
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
}
|
||||
|
||||
// Generate secure token.
|
||||
// Token
|
||||
try {
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
$token = bin2hex(random_bytes(16));
|
||||
} catch (Exception $e) {
|
||||
return ["error" => "Could not generate token."];
|
||||
}
|
||||
|
||||
// Calculate expiration time.
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
// Expiry
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
// Password hash
|
||||
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// Define the share folder links file.
|
||||
// Load existing
|
||||
$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 = [];
|
||||
$links = file_exists($shareFile)
|
||||
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||
: [];
|
||||
|
||||
// Cleanup
|
||||
$now = time();
|
||||
foreach ($links as $k => $v) {
|
||||
if (!empty($v['expires']) && $v['expires'] < $now) {
|
||||
unset($links[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new share record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
// Add new
|
||||
$links[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
"allowUpload" => $allowUpload
|
||||
];
|
||||
|
||||
// Save the updated share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
||||
// Save
|
||||
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||
return ["error" => "Could not save share link."];
|
||||
}
|
||||
|
||||
// Determine the base URL.
|
||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
||||
$baseUrl = rtrim(BASE_URL, '/');
|
||||
} else {
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
||||
$baseUrl = $protocol . "://" . $host;
|
||||
}
|
||||
// The share URL points to the shared folder page.
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
// Build URL
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname());
|
||||
$baseUrl = $protocol . '://' . rtrim($host, '/');
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
return ["token" => $token, "expires" => $expires, "link" => $link];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieves information for a shared file from a shared folder link.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -418,7 +419,8 @@ class FolderModel {
|
||||
* - "realFilePath": the absolute path to the file,
|
||||
* - "mimeType": the detected MIME type.
|
||||
*/
|
||||
public static function getSharedFileInfo(string $token, string $file): array {
|
||||
public static function getSharedFileInfo(string $token, string $file): array
|
||||
{
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -457,14 +459,14 @@ class FolderModel {
|
||||
return ["error" => "Invalid file name."];
|
||||
}
|
||||
$file = basename($file);
|
||||
|
||||
|
||||
// Build the full file path.
|
||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||
$realFilePath = realpath($filePath);
|
||||
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
|
||||
return ["error" => "File not found."];
|
||||
}
|
||||
|
||||
|
||||
$mimeType = mime_content_type($realFilePath);
|
||||
return [
|
||||
"realFilePath" => $realFilePath,
|
||||
@@ -479,11 +481,12 @@ class FolderModel {
|
||||
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
||||
* @return array An associative array with "success" on success or "error" on failure.
|
||||
*/
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array {
|
||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array
|
||||
{
|
||||
// Define maximum file size and allowed extensions.
|
||||
$maxSize = 50 * 1024 * 1024; // 50 MB
|
||||
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv'];
|
||||
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'mp4', 'webm', 'mp3', 'mkv'];
|
||||
|
||||
// Load the share folder record.
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
@@ -494,55 +497,55 @@ class FolderModel {
|
||||
return ["error" => "Invalid share token."];
|
||||
}
|
||||
$record = $shareLinks[$token];
|
||||
|
||||
|
||||
// Check expiration.
|
||||
if (time() > $record['expires']) {
|
||||
return ["error" => "This share link has expired."];
|
||||
}
|
||||
|
||||
|
||||
// Check whether uploads are allowed.
|
||||
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
|
||||
return ["error" => "File uploads are not allowed for this share."];
|
||||
}
|
||||
|
||||
|
||||
// Validate file upload presence.
|
||||
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
|
||||
return ["error" => "File upload error. Code: " . $fileUpload['error']];
|
||||
}
|
||||
|
||||
|
||||
if ($fileUpload['size'] > $maxSize) {
|
||||
return ["error" => "File size exceeds allowed limit."];
|
||||
}
|
||||
|
||||
|
||||
$uploadedName = basename($fileUpload['name']);
|
||||
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
return ["error" => "File type not allowed."];
|
||||
}
|
||||
|
||||
|
||||
// Determine the target folder from the share record.
|
||||
$folderName = trim($record['folder'], "/\\");
|
||||
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!empty($folderName) && strtolower($folderName) !== 'root') {
|
||||
$targetFolder .= $folderName;
|
||||
}
|
||||
|
||||
|
||||
// Verify target folder exists.
|
||||
$realTargetFolder = realpath($targetFolder);
|
||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
|
||||
|
||||
// Generate a new filename (using uniqid and sanitizing the original name).
|
||||
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
|
||||
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
|
||||
|
||||
|
||||
// Move the uploaded file.
|
||||
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
||||
return ["error" => "Failed to move the uploaded file."];
|
||||
}
|
||||
|
||||
|
||||
// --- Metadata Update ---
|
||||
// Determine metadata file.
|
||||
$metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName;
|
||||
@@ -564,7 +567,7 @@ class FolderModel {
|
||||
"uploader" => $uploader
|
||||
];
|
||||
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
||||
|
||||
|
||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user