Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f39b3a41e | ||
|
|
40cecc10ad | ||
|
|
aee78c9750 | ||
|
|
16ccb66d55 |
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 5/27/2025 v1.3.9
|
||||
|
||||
- Support for mounting CIFS (SMB) network shares via Docker volumes
|
||||
- New `scripts/scan_uploads.php` script to generate metadata for imported files and folders
|
||||
- `SCAN_ON_START` environment variable to trigger automatic scanning on container startup
|
||||
- Documentation for configuring CIFS share mounting and scanning
|
||||
|
||||
- Clipboard Paste Upload Support (single image):
|
||||
- Users can now paste images directly into the FileRise web interface.
|
||||
- Pasted images are renamed to `image<TIMESTAMP>.png` and added to the upload queue using the existing drag-and-drop logic.
|
||||
- Implemented using a `.isClipboard` flag and a delayed UI cleanup inside `xhr.addEventListener("load", ...)`.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/26/2025
|
||||
|
||||
- Updated `REGEX_FOLDER_NAME` in `config.php` to forbids < > : " | ? * characters in folder names.
|
||||
- Ensures the whole name can’t end in a space or period.
|
||||
- Blocks Windows device names.
|
||||
|
||||
- Updated `FolderController.php` when `createFolder` issues invalid folder name to return `http_response_code(400);`
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/23/2025 v1.3.8
|
||||
|
||||
- **Folder-strip context menu**
|
||||
- Enabled right-click on items in the new folder strip (above file list) to open the same “Create / Rename / Share / Delete Folder” menu as in the main folder tree.
|
||||
- Bound `contextmenu` event on each `.folder-item` in `loadFileList` to:
|
||||
- Prevent the default browser menu
|
||||
- Highlight the clicked folder-strip item
|
||||
- Invoke `showFolderManagerContextMenu` with menu entries:
|
||||
- Create Folder
|
||||
- Rename Folder
|
||||
- Share Folder (passes the strip’s `data-folder` value)
|
||||
- Delete Folder
|
||||
- Ensured menu actions are wrapped in arrow functions (`() => …`) so they fire only on menu-item click, not on render.
|
||||
|
||||
- Refactored folder-strip injection in `fileListView.js` to:
|
||||
- Mark each strip item as `draggable="true"` (for drag-and-drop)
|
||||
- Add `el.addEventListener("contextmenu", …)` alongside existing click/drag handlers
|
||||
- Clean up global click listener for hiding the context menu
|
||||
|
||||
- Prevented premature invocation of `openFolderShareModal` by switching to `action: () => openFolderShareModal(dest)` instead of calling it directly.
|
||||
|
||||
- **Create File/Folder dropdown**
|
||||
- Replaced standalone “Create File” button with a combined dropdown button in the actions toolbar.
|
||||
- New markup
|
||||
- Wired up JS handlers in `fileActions.js`:
|
||||
- `#createFileOption` → `openCreateFileModal()`
|
||||
- `#createFolderOption` → `document.getElementById('createFolderModal').style.display = 'block'`
|
||||
- Toggled `.dropdown-menu` visibility on button click, and closed on outside click.
|
||||
- Applied dark-mode support: dropdown background and text colors switch with `.dark-mode` class.
|
||||
|
||||
---
|
||||
|
||||
## Changes 5/22/2025 v1.3.7
|
||||
|
||||
- `.folder-strip-container .folder-name` css added to center text below folder material icon.
|
||||
|
||||
@@ -28,7 +28,7 @@ define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT','m/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE','5G');
|
||||
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
||||
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
|
||||
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||
|
||||
@@ -848,11 +848,27 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
||||
background-color: #00796B;
|
||||
}
|
||||
|
||||
#createFileBtn {
|
||||
#createBtn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark-mode .dropdown-menu {
|
||||
background-color: #2c2c2c !important;
|
||||
border-color: #444 !important;
|
||||
color: #e0e0e0!important;
|
||||
}
|
||||
body.dark-mode .dropdown-menu .dropdown-item {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
body.dark-mode .dropdown-item:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
#fileList button.edit-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
|
||||
@@ -391,9 +391,36 @@
|
||||
data-i18n-key="download_zip">Download ZIP</button>
|
||||
<button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip"
|
||||
data-i18n-key="extract_zip_button">Extract Zip</button>
|
||||
<button id="createFileBtn" class="btn action-btn" data-i18n-key="create_file">
|
||||
${t('create_file')}
|
||||
</button>
|
||||
<div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
|
||||
<button id="createBtn" class="btn action-btn" data-i18n-key="create">
|
||||
${t('create')} <span class="material-icons" style="font-size:16px;vertical-align:middle;">arrow_drop_down</span>
|
||||
</button>
|
||||
<ul
|
||||
id="createMenu"
|
||||
class="dropdown-menu"
|
||||
style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
min-width: 140px;
|
||||
"
|
||||
>
|
||||
<li id="createFileOption" class="dropdown-item" data-i18n-key="create_file" style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_file')}
|
||||
</li>
|
||||
<li id="createFolderOption" class="dropdown-item" data-i18n-key="create_folder" style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_folder')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Create File Modal -->
|
||||
<div id="createFileModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.3.7";
|
||||
const version = "v1.3.9";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
// ————— Inject updated styles —————
|
||||
|
||||
@@ -39,7 +39,7 @@ export function updateFileActionButtons() {
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
const zipBtn = document.getElementById("downloadZipBtn");
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
const createBtn = document.getElementById("createFileBtn");
|
||||
const createBtn = document.getElementById("createBtn");
|
||||
|
||||
const anyFiles = fileCheckboxes.length > 0;
|
||||
const anySelected = selectedCheckboxes.length > 0;
|
||||
|
||||
@@ -688,4 +688,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('createBtn');
|
||||
const menu = document.getElementById('createMenu');
|
||||
const fileOpt = document.getElementById('createFileOption');
|
||||
const folderOpt= document.getElementById('createFolderOption');
|
||||
|
||||
// Toggle dropdown on click
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// Create File
|
||||
fileOpt.addEventListener('click', () => {
|
||||
menu.style.display = 'none';
|
||||
openCreateFileModal(); // your existing function
|
||||
});
|
||||
|
||||
// Create Folder
|
||||
folderOpt.addEventListener('click', () => {
|
||||
menu.style.display = 'none';
|
||||
document.getElementById('createFolderModal').style.display = 'block';
|
||||
document.getElementById('newFolderName').focus();
|
||||
});
|
||||
|
||||
// Close if you click anywhere else
|
||||
document.addEventListener('click', () => {
|
||||
menu.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
window.renameFile = renameFile;
|
||||
@@ -16,7 +16,16 @@ import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation } from './folderManager.js';
|
||||
import {
|
||||
getParentFolder,
|
||||
updateBreadcrumbTitle,
|
||||
setupBreadcrumbDelegation,
|
||||
showFolderManagerContextMenu,
|
||||
hideFolderManagerContextMenu,
|
||||
openRenameFolderModal,
|
||||
openDeleteFolderModal
|
||||
} from './folderManager.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
import {
|
||||
folderDragOverHandler,
|
||||
folderDragLeaveHandler,
|
||||
@@ -240,17 +249,40 @@ export async function loadFileList(folderParam) {
|
||||
if (!data.files || Object.keys(data.files).length === 0) {
|
||||
fileListContainer.textContent = t("no_files_found");
|
||||
|
||||
// hide summary
|
||||
// hide summary + slider
|
||||
const summaryElem = document.getElementById("fileSummary");
|
||||
if (summaryElem) summaryElem.style.display = "none";
|
||||
|
||||
// hide slider
|
||||
const sliderContainer = document.getElementById("viewSliderContainer");
|
||||
if (sliderContainer) sliderContainer.style.display = "none";
|
||||
|
||||
// hide folder strip
|
||||
const strip = document.getElementById("folderStripContainer");
|
||||
if (strip) strip.style.display = "none";
|
||||
// show/hide folder strip *even when there are no files*
|
||||
let strip = document.getElementById("folderStripContainer");
|
||||
if (!strip) {
|
||||
strip = document.createElement("div");
|
||||
strip.id = "folderStripContainer";
|
||||
strip.className = "folder-strip-container";
|
||||
actionsContainer.parentNode.insertBefore(strip, fileListContainer);
|
||||
}
|
||||
if (window.showFoldersInList && subfolders.length) {
|
||||
strip.innerHTML = subfolders.map(sf => `
|
||||
<div class="folder-item" data-folder="${sf.full}">
|
||||
<i class="material-icons">folder</i>
|
||||
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
strip.style.display = "flex";
|
||||
strip.querySelectorAll(".folder-item").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
const dest = el.dataset.folder;
|
||||
window.currentFolder = dest;
|
||||
localStorage.setItem("lastOpenedFolder", dest);
|
||||
updateBreadcrumbTitle(dest);
|
||||
loadFileList(dest);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
strip.style.display = "none";
|
||||
}
|
||||
|
||||
updateFileActionButtons();
|
||||
return [];
|
||||
@@ -360,34 +392,73 @@ export async function loadFileList(folderParam) {
|
||||
strip.className = "folder-strip-container";
|
||||
actionsContainer.parentNode.insertBefore(strip, actionsContainer);
|
||||
}
|
||||
|
||||
if (window.showFoldersInList && subfolders.length) {
|
||||
strip.innerHTML = subfolders.map(sf => `
|
||||
<div class="folder-item" data-folder="${sf.full}" draggable="true">
|
||||
<i class="material-icons">folder</i>
|
||||
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
<div class="folder-item" data-folder="${sf.full}" draggable="true">
|
||||
<i class="material-icons">folder</i>
|
||||
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
strip.style.display = "flex";
|
||||
|
||||
// wire up each folder‐tile
|
||||
strip.querySelectorAll(".folder-item").forEach(el => {
|
||||
// click‐to‐navigate
|
||||
// 1) click to navigate
|
||||
el.addEventListener("click", () => {
|
||||
const dest = el.dataset.folder;
|
||||
window.currentFolder = dest;
|
||||
localStorage.setItem("lastOpenedFolder", dest);
|
||||
updateBreadcrumbTitle(dest);
|
||||
document.querySelectorAll(".folder-option.selected")
|
||||
.forEach(o => o.classList.remove("selected"));
|
||||
document.querySelector(`.folder-option[data-folder="${dest}"]`)
|
||||
?.classList.add("selected");
|
||||
document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected"));
|
||||
document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected");
|
||||
loadFileList(dest);
|
||||
});
|
||||
|
||||
// drag & drop handlers
|
||||
// 2) drag & drop
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
|
||||
// 3) right-click context menu
|
||||
el.addEventListener("contextmenu", e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const dest = el.dataset.folder;
|
||||
window.currentFolder = dest;
|
||||
localStorage.setItem("lastOpenedFolder", dest);
|
||||
|
||||
// highlight the strip tile
|
||||
strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected"));
|
||||
el.classList.add("selected");
|
||||
|
||||
// reuse folderManager menu
|
||||
const menuItems = [
|
||||
{
|
||||
label: t("create_folder"),
|
||||
action: () => document.getElementById("createFolderModal").style.display = "block"
|
||||
},
|
||||
{
|
||||
label: t("rename_folder"),
|
||||
action: () => openRenameFolderModal()
|
||||
},
|
||||
{
|
||||
label: t("folder_share"),
|
||||
action: () => openFolderShareModal(dest)
|
||||
},
|
||||
{
|
||||
label: t("delete_folder"),
|
||||
action: () => openDeleteFolderModal()
|
||||
}
|
||||
];
|
||||
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
||||
});
|
||||
});
|
||||
|
||||
// one global click to hide any open context menu
|
||||
document.addEventListener("click", hideFolderManagerContextMenu);
|
||||
|
||||
} else {
|
||||
strip.style.display = "none";
|
||||
}
|
||||
|
||||
@@ -551,7 +551,7 @@ export function loadFolderList(selectedFolder) {
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||
|
||||
function openRenameFolderModal() {
|
||||
export function openRenameFolderModal() {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
showToast("Please select a valid folder to rename.");
|
||||
@@ -614,7 +614,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
|
||||
});
|
||||
});
|
||||
|
||||
function openDeleteFolderModal() {
|
||||
export function openDeleteFolderModal() {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
showToast("Please select a valid folder to delete.");
|
||||
@@ -718,7 +718,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", async ()
|
||||
});
|
||||
|
||||
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
|
||||
function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
export function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("folderManagerContextMenu");
|
||||
if (!menu) {
|
||||
menu = document.createElement("div");
|
||||
@@ -765,7 +765,7 @@ function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
menu.style.display = "block";
|
||||
}
|
||||
|
||||
function hideFolderManagerContextMenu() {
|
||||
export function hideFolderManagerContextMenu() {
|
||||
const menu = document.getElementById("folderManagerContextMenu");
|
||||
if (menu) {
|
||||
menu.style.display = "none";
|
||||
@@ -796,7 +796,7 @@ function folderManagerContextMenuHandler(e) {
|
||||
},
|
||||
{
|
||||
label: t("folder_share"),
|
||||
action: () => { openFolderShareModal(); }
|
||||
action: () => { openFolderShareModal(folder); }
|
||||
},
|
||||
{
|
||||
label: t("delete_folder"),
|
||||
|
||||
@@ -669,6 +669,18 @@ function submitFiles(allFiles) {
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
if (file.isClipboard) {
|
||||
setTimeout(() => {
|
||||
window.selectedFiles = [];
|
||||
updateFileInfoCount();
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
if (progressContainer) progressContainer.innerHTML = "";
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
if (fileInfoContainer) {
|
||||
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ─── Only now count this chunk as finished ───────────────────
|
||||
finishedCount++;
|
||||
@@ -848,3 +860,38 @@ function initUpload() {
|
||||
}
|
||||
|
||||
export { initUpload };
|
||||
|
||||
// -------------------------
|
||||
// Clipboard Paste Handler (Mimics Drag-and-Drop)
|
||||
// -------------------------
|
||||
document.addEventListener('paste', function handlePasteUpload(e) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
const files = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const ext = file.name.split('.').pop() || 'png';
|
||||
const renamedFile = new File([file], `image${Date.now()}.${ext}`, { type: file.type });
|
||||
renamedFile.isClipboard = true;
|
||||
|
||||
Object.defineProperty(renamedFile, 'customRelativePath', {
|
||||
value: renamedFile.name,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
files.push(renamedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
showToast('Pasted file added to upload list.', 'success');
|
||||
}
|
||||
});
|
||||
63
scripts/scan_uploads.php
Normal file
63
scripts/scan_uploads.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* scan_uploads.php
|
||||
* Scans the uploads directory and creates metadata entries for new files/folders using config settings.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
if (!isset($config['upload_dir']) || !isset($config['metadata_dir'])) {
|
||||
die("Missing configuration for upload_dir or metadata_dir\n");
|
||||
}
|
||||
|
||||
$uploadDir = $config['upload_dir'];
|
||||
$metadataDir = $config['metadata_dir'];
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
function scanDirectory($dir) {
|
||||
$items = array_diff(scandir($dir), ['.', '..']);
|
||||
$results = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
$results[] = $path;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$results = array_merge($results, scanDirectory($path));
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function metadataPath($filePath, $uploadDir, $metadataDir) {
|
||||
$relativePath = ltrim(str_replace($uploadDir, '', $filePath), '/');
|
||||
return $metadataDir . '/' . $relativePath . '.json';
|
||||
}
|
||||
|
||||
$allItems = scanDirectory($uploadDir);
|
||||
|
||||
foreach ($allItems as $item) {
|
||||
$metaPath = metadataPath($item, $uploadDir, $metadataDir);
|
||||
|
||||
if (!file_exists($metaPath)) {
|
||||
$type = is_dir($item) ? 'folder' : 'file';
|
||||
$size = is_file($item) ? filesize($item) : 0;
|
||||
|
||||
$metadata = [
|
||||
'path' => str_replace($uploadDir, '', $item),
|
||||
'type' => $type,
|
||||
'size' => $size,
|
||||
'user' => 'Imported',
|
||||
'uploadDate' => date('c')
|
||||
];
|
||||
|
||||
if (!is_dir(dirname($metaPath))) {
|
||||
mkdir(dirname($metaPath), 0775, true);
|
||||
}
|
||||
|
||||
file_put_contents($metaPath, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
echo "Created metadata for: {$item}\n";
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -96,12 +96,14 @@ class FolderController
|
||||
|
||||
// Basic sanitation for folderName.
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optionally sanitize the parent.
|
||||
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid parent folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user