New fetchWithCsrf with fallback for session change. start.sh session directory added.
This commit is contained in:
@@ -44,6 +44,55 @@ function showToast(msgKey) {
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
/*
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function fetchWithCsrf(url, options = {}) {
|
||||
options = { credentials: 'include', headers: {}, ...options };
|
||||
options.headers['X-CSRF-Token'] = window.csrfToken;
|
||||
|
||||
// 1) First attempt using the original fetch
|
||||
let res = await originalFetch(url, options);
|
||||
|
||||
// 2) Soft‐failure JSON check (200 + {csrf_expired})
|
||||
if (res.ok && res.headers.get('content-type')?.includes('application/json')) {
|
||||
const clone = res.clone();
|
||||
const data = await clone.json();
|
||||
if (data.csrf_expired) {
|
||||
const newToken = data.csrf_token;
|
||||
window.csrfToken = newToken;
|
||||
document.querySelector('meta[name="csrf-token"]').content = newToken;
|
||||
options.headers['X-CSRF-Token'] = newToken;
|
||||
return originalFetch(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) HTTP 403 fallback
|
||||
if (res.status === 403) {
|
||||
let newToken = res.headers.get('X-CSRF-Token');
|
||||
if (!newToken) {
|
||||
const tokRes = await originalFetch('/api/auth/token.php', { credentials: 'include' });
|
||||
if (tokRes.ok) {
|
||||
const body = await tokRes.json();
|
||||
newToken = body.csrf_token;
|
||||
}
|
||||
}
|
||||
if (newToken) {
|
||||
window.csrfToken = newToken;
|
||||
document.querySelector('meta[name="csrf-token"]').content = newToken;
|
||||
options.headers['X-CSRF-Token'] = newToken;
|
||||
res = await originalFetch(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||
function openTOTPLoginModal() {
|
||||
originalOpenTOTPLoginModal();
|
||||
@@ -236,6 +285,10 @@ function checkAuthentication(showLoginToast = true) {
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
if (data.csrf_token) {
|
||||
window.csrfToken = data.csrf_token;
|
||||
document.querySelector('meta[name="csrf-token"]').content = data.csrf_token;
|
||||
}
|
||||
updateAuthenticatedUI(data);
|
||||
return data;
|
||||
} else {
|
||||
@@ -277,11 +330,11 @@ async function submitLogin(data) {
|
||||
try {
|
||||
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||
if (perm && typeof perm === "object") {
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload", perm.disableUpload ? "true" : "false");
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
return window.location.reload();
|
||||
}
|
||||
|
||||
@@ -406,10 +459,10 @@ function initAuth() {
|
||||
}
|
||||
let url = "/api/addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetch(url, {
|
||||
fetchWithCsrf(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -439,10 +492,10 @@ function initAuth() {
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) return;
|
||||
fetch("/api/removeUser.php", {
|
||||
fetchWithCsrf("/api/removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -478,10 +531,10 @@ function initAuth() {
|
||||
return;
|
||||
}
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
fetch("/api/changePassword.php", {
|
||||
fetchWithCsrf("/api/changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
|
||||
const version = "v1.2.3"; // Update this version string as needed
|
||||
const version = "v1.2.4"; // Update this version string as needed
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { loadFileList } from './fileListView.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
import { fetchWithCsrf } from './auth.js';
|
||||
import { loadCsrfToken } from './main.js';
|
||||
|
||||
/* ----------------------
|
||||
Helper Functions (Data/State)
|
||||
@@ -337,7 +339,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
try {
|
||||
// Check if the user has folder-only permission.
|
||||
await checkUserFolderPermission();
|
||||
|
||||
|
||||
// Determine effective root folder.
|
||||
const username = localStorage.getItem("username") || "root";
|
||||
let effectiveRoot = "root";
|
||||
@@ -351,14 +353,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
|
||||
|
||||
// Build fetch URL.
|
||||
let fetchUrl = '/api/folder/getFolderList.php';
|
||||
if (window.userFolderOnly) {
|
||||
fetchUrl += '?restricted=1';
|
||||
}
|
||||
console.log("Fetching folder list from:", fetchUrl);
|
||||
|
||||
|
||||
// Fetch folder list from the server.
|
||||
const response = await fetch(fetchUrl);
|
||||
if (response.status === 401) {
|
||||
@@ -375,10 +377,10 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else if (Array.isArray(folderData)) {
|
||||
folders = folderData;
|
||||
}
|
||||
|
||||
|
||||
// Remove any global "root" entry.
|
||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||
|
||||
|
||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||
@@ -386,16 +388,16 @@ export async function loadFolderTree(selectedFolder) {
|
||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||
window.currentFolder = effectiveRoot;
|
||||
}
|
||||
|
||||
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
|
||||
// Render the folder tree.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (!container) {
|
||||
console.error("Folder tree container not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||
@@ -405,35 +407,35 @@ export async function loadFolderTree(selectedFolder) {
|
||||
html += renderFolderTree(tree, "", "block");
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
|
||||
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
setupBreadcrumbDelegation();
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
|
||||
const folderState = loadFolderTreeState();
|
||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||
expandTreePath(window.currentFolder);
|
||||
}
|
||||
|
||||
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -448,7 +450,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
loadFileList(selected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||
if (rootToggle) {
|
||||
rootToggle.addEventListener("click", function (e) {
|
||||
@@ -471,7 +473,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -494,7 +496,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading folder tree:", error);
|
||||
}
|
||||
@@ -627,45 +629,53 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
|
||||
document.getElementById("newFolderName").value = "";
|
||||
});
|
||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", async () => {
|
||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||
if (!folderInput) {
|
||||
showToast("Please enter a folder name.");
|
||||
return;
|
||||
if (!folderInput) return showToast("Please enter a folder name.");
|
||||
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const parent = selectedFolder === "root" ? "" : selectedFolder;
|
||||
|
||||
// 1) Guarantee fresh CSRF
|
||||
try {
|
||||
await loadCsrfToken();
|
||||
} catch {
|
||||
return showToast("Could not refresh CSRF token. Please reload.");
|
||||
}
|
||||
let selectedFolder = window.currentFolder || "root";
|
||||
let fullFolderName = folderInput;
|
||||
if (selectedFolder && selectedFolder !== "root") {
|
||||
fullFolderName = selectedFolder + "/" + folderInput;
|
||||
}
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch("/api/folder/createFolder.php", {
|
||||
|
||||
// 2) Call with fetchWithCsrf
|
||||
fetchWithCsrf("/api/folder/createFolder.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderName: folderInput,
|
||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
||||
})
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderName: folderInput, parent })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder created successfully!");
|
||||
window.currentFolder = fullFolderName;
|
||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
||||
loadFolderList(fullFolderName);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not create folder"));
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
// pull out a JSON error, or fallback to status text
|
||||
let err;
|
||||
try {
|
||||
const j = await res.json();
|
||||
err = j.error || j.message || res.statusText;
|
||||
} catch {
|
||||
err = res.statusText;
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
showToast("Folder created!");
|
||||
const full = parent ? `${parent}/${folderInput}` : folderInput;
|
||||
window.currentFolder = full;
|
||||
localStorage.setItem("lastOpenedFolder", full);
|
||||
loadFolderList(full);
|
||||
})
|
||||
.catch(e => {
|
||||
showToast("Error creating folder: " + e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
document.getElementById("newFolderName").value = "";
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error creating folder:", error);
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
const _originalFetch = window.fetch;
|
||||
window.fetch = fetchWithCsrf;
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
@@ -13,35 +15,53 @@ import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
||||
function loadCsrfToken() {
|
||||
return fetch('/api/auth/token.php', { credentials: 'include' })
|
||||
/**
|
||||
* Fetches the current CSRF token (and share URL), updates window globals
|
||||
* and <meta> tags, and returns the data.
|
||||
*
|
||||
* @returns {Promise<{csrf_token: string, share_url: string}>}
|
||||
*/
|
||||
export function loadCsrfToken() {
|
||||
return fetch('/api/auth/token.php', {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Token fetch failed with status: " + response.status);
|
||||
throw new Error(`Token fetch failed with status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
// Prefer header if set, otherwise fall back to body
|
||||
const headerToken = response.headers.get('X-CSRF-Token');
|
||||
return response.json()
|
||||
.then(body => ({
|
||||
csrf_token: headerToken || body.csrf_token,
|
||||
share_url: body.share_url
|
||||
}));
|
||||
})
|
||||
.then(data => {
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
.then(({ csrf_token, share_url }) => {
|
||||
// Update globals
|
||||
window.csrfToken = csrf_token;
|
||||
window.SHARE_URL = share_url;
|
||||
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
// Sync <meta name="csrf-token">
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
meta.content = csrf_token;
|
||||
|
||||
return data;
|
||||
// Sync <meta name="share-url">
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = share_url;
|
||||
|
||||
return { csrf_token, share_url };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -412,7 +412,12 @@ function initResumableUpload() {
|
||||
forceChunkSize: true,
|
||||
testChunks: false,
|
||||
throttleProgressCallbacks: 1,
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
withCredentials: true,
|
||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||
query: {
|
||||
folder: window.currentFolder || "root",
|
||||
upload_token: window.csrfToken // still as a fallback
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("file");
|
||||
@@ -496,26 +501,40 @@ function initResumableUpload() {
|
||||
});
|
||||
|
||||
resumableInstance.on("fileSuccess", function(file, message) {
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
// Try to parse JSON response
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
// 1) Soft‐fail CSRF? then update token & retry this file
|
||||
if (data && data.csrf_expired) {
|
||||
// Update global and Resumable headers
|
||||
window.csrfToken = data.csrf_token;
|
||||
resumableInstance.opts.headers['X-CSRF-Token'] = data.csrf_token;
|
||||
resumableInstance.opts.query.upload_token = data.csrf_token;
|
||||
// Retry this chunk/file
|
||||
file.retry();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Otherwise treat as real success:
|
||||
const li = document.querySelector(
|
||||
`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`
|
||||
);
|
||||
if (li && li.progressBar) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
// Hide pause/resume and remove buttons for successful files.
|
||||
// remove action buttons
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.style.display = "none";
|
||||
}
|
||||
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||
const removeBtn = li.querySelector(".remove-file-btn");
|
||||
if (removeBtn) {
|
||||
removeBtn.style.display = "none";
|
||||
}
|
||||
// Schedule removal of the file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
||||
updateFileInfoCount();
|
||||
}, 5000);
|
||||
if (removeBtn) removeBtn.style.display = "none";
|
||||
setTimeout(() => li.remove(), 5000);
|
||||
}
|
||||
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
@@ -618,8 +637,25 @@ function submitFiles(allFiles) {
|
||||
} catch (e) {
|
||||
jsonResponse = null;
|
||||
}
|
||||
|
||||
// ─── Soft-fail CSRF: retry this upload ───────────────────────
|
||||
if (jsonResponse && jsonResponse.csrf_expired) {
|
||||
console.warn("CSRF expired during upload, retrying chunk", file.uploadIndex);
|
||||
// 1) update global token + header
|
||||
window.csrfToken = jsonResponse.csrf_token;
|
||||
xhr.open("POST", "/api/upload/upload.php", true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
// 2) re-send the same formData
|
||||
xhr.send(formData);
|
||||
return; // skip the "finishedCount++" and error/success logic for now
|
||||
}
|
||||
|
||||
// ─── Normal success/error handling ────────────────────────────
|
||||
const li = progressElements[file.uploadIndex];
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
// real success
|
||||
if (li) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
@@ -627,11 +663,14 @@ function submitFiles(allFiles) {
|
||||
}
|
||||
uploadResults[file.uploadIndex] = true;
|
||||
} else {
|
||||
// real failure
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
|
||||
// ─── Only now count this chunk as finished ───────────────────
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
@@ -665,6 +704,7 @@ function submitFiles(allFiles) {
|
||||
});
|
||||
|
||||
xhr.open("POST", "/api/upload/upload.php", true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user