release(v1.7.0): asset cache-busting pipeline, public siteConfig cache, JS core split, and caching/security polish
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// adminPanel.js
|
||||
import { t } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
|
||||
const version = window.APP_VERSION || "dev";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
160
public/js/appCore.js
Normal file
160
public/js/appCore.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// /js/appCore.js
|
||||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||||
import { initTagSearch } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
|
||||
// Keep a bound handle to the native fetch so wrappers elsewhere never recurse
|
||||
const _nativeFetch = window.fetch.bind(window);
|
||||
|
||||
/* =========================
|
||||
CSRF UTILITIES (shared)
|
||||
========================= */
|
||||
export function setCsrfToken(token) {
|
||||
if (!token) return;
|
||||
window.csrfToken = token;
|
||||
localStorage.setItem('csrf', token);
|
||||
|
||||
// meta tag for easy access in other places
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.content = token;
|
||||
}
|
||||
|
||||
export function getCsrfToken() {
|
||||
return window.csrfToken || localStorage.getItem('csrf') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap/refresh CSRF from the server.
|
||||
* Uses the native fetch to avoid wrapper loops and accepts rotated tokens via header.
|
||||
*/
|
||||
export async function loadCsrfToken() {
|
||||
const res = await _nativeFetch('/api/auth/token.php', { method: 'GET', credentials: 'include' });
|
||||
|
||||
// header-based rotation
|
||||
const hdr = res.headers.get('X-CSRF-Token');
|
||||
if (hdr) setCsrfToken(hdr);
|
||||
|
||||
// body (if provided)
|
||||
let body = {};
|
||||
try { body = await res.json(); } catch { /* token endpoint may return empty */ }
|
||||
|
||||
const token = body.csrf_token || getCsrfToken();
|
||||
setCsrfToken(token);
|
||||
|
||||
// share-url meta should reflect the actual origin
|
||||
const actualShare = window.location.origin;
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = actualShare;
|
||||
|
||||
return { csrf_token: token, share_url: actualShare };
|
||||
}
|
||||
|
||||
/* =========================
|
||||
APP INIT (shared)
|
||||
========================= */
|
||||
export function initializeApp() {
|
||||
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||
|
||||
const last = localStorage.getItem('lastOpenedFolder');
|
||||
window.currentFolder = last ? last : "root";
|
||||
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||
|
||||
// Load public site config early (safe subset)
|
||||
loadAdminConfigFunc();
|
||||
|
||||
// Enable tag search UI; initial file list load is controlled elsewhere
|
||||
initTagSearch();
|
||||
|
||||
// Hook DnD relay from fileList area into upload area
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
if (fileListArea && uploadArea) {
|
||||
fileListArea.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.add('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('dragleave', () => {
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||
dataTransfer: e.dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// App subsystems
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
|
||||
// Only run trash/restore for admins
|
||||
const isAdmin =
|
||||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||||
if (isAdmin) {
|
||||
setupTrashRestoreDelete();
|
||||
}
|
||||
|
||||
// Small help tooltip toggle
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
if (helpBtn && helpTooltip) {
|
||||
helpBtn.addEventListener("click", () => {
|
||||
helpTooltip.style.display =
|
||||
helpTooltip.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
LOGOUT (shared)
|
||||
========================= */
|
||||
export function triggerLogout() {
|
||||
_nativeFetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(() => { /* no-op */ });
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Global UX guard (unchanged)
|
||||
========================= */
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
const msg = (ev?.reason && ev.reason.message) || "";
|
||||
if (msg === "auth") {
|
||||
showToast(t("please_sign_in_again") || "Please sign in again.", "error");
|
||||
ev.preventDefault();
|
||||
} else if (msg === "forbidden") {
|
||||
showToast(t("no_access_to_resource") || "You don’t have access to that.", "error");
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations } from './i18n.js';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations } from './i18n.js?v={{APP_QVER}}';
|
||||
import {
|
||||
toggleVisibility,
|
||||
showToast as originalShowToast,
|
||||
attachEnterKeyListener,
|
||||
showCustomConfirmModal
|
||||
} from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import { renderFileTable } from './fileListView.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
} from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { renderFileTable } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import {
|
||||
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
||||
openUserPanel,
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
closeTOTPModal,
|
||||
setLastLoginData,
|
||||
openApiModal
|
||||
} from './authModals.js';
|
||||
import { openAdminPanel } from './adminPanel.js';
|
||||
import { initializeApp, triggerLogout } from './main.js';
|
||||
} from './authModals.js?v={{APP_QVER}}';
|
||||
import { openAdminPanel } from './adminPanel.js?v={{APP_QVER}}';
|
||||
import { initializeApp, triggerLogout } from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
@@ -180,7 +180,7 @@ function updateLoginOptionsUIFromStorage() {
|
||||
}
|
||||
|
||||
export function loadAdminConfigFunc() {
|
||||
return fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||
return fetch("/api/siteConfig.php", { credentials: "include" })
|
||||
.then(async (response) => {
|
||||
// If a proxy or some edge returns 204/empty, handle gracefully
|
||||
let config = {};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js?v={{APP_QVER}}';
|
||||
|
||||
let lastLoginData = null;
|
||||
export function setLastLoginData(data) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// domUtils.js
|
||||
import { t } from './i18n.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
|
||||
// Basic DOM Helpers
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
|
||||
@@ -489,18 +489,12 @@ function mountHeaderToggle(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function ensureZonesToggle() {
|
||||
const isAuthed = document.body.classList.contains('authenticated');
|
||||
let btn = document.getElementById('sidebarToggleFloating');
|
||||
const host = getHeaderHost();
|
||||
if (!host) return;
|
||||
|
||||
// If not authenticated, make sure the button is gone and bail.
|
||||
if (!isAuthed) {
|
||||
if (btn) btn.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure the host is a positioning context
|
||||
const hostStyle = getComputedStyle(host);
|
||||
if (hostStyle.position === 'static') {
|
||||
@@ -542,6 +536,7 @@ function ensureZonesToggle() {
|
||||
padding: '0',
|
||||
lineHeight: '0'
|
||||
});
|
||||
btn.classList.add('zones-toggle');
|
||||
|
||||
// Dark mode polish
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// fileActions.js
|
||||
import { showToast, attachEnterKeyListener } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { formatFolderName } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { showToast, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { formatFolderName } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function handleDeleteSelected(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// fileDragDrop.js
|
||||
import { showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
|
||||
export function fileDragStartHandler(event) {
|
||||
const row = event.currentTarget;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// fileEditor.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
// thresholds for editor behavior
|
||||
const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings
|
||||
@@ -14,30 +14,30 @@ const CM_LOCAL = "/vendor/codemirror/5.65.5/";
|
||||
// Which mode file to load for a given name/mime
|
||||
const MODE_URL = {
|
||||
// core/common
|
||||
"xml": "mode/xml/xml.min.js",
|
||||
"css": "mode/css/css.min.js",
|
||||
"javascript": "mode/javascript/javascript.min.js",
|
||||
"xml": "mode/xml/xml.min.js?v={{APP_QVER}}",
|
||||
"css": "mode/css/css.min.js?v={{APP_QVER}}",
|
||||
"javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}",
|
||||
|
||||
// meta / combos
|
||||
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js",
|
||||
"application/x-httpd-php": "mode/php/php.min.js",
|
||||
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}",
|
||||
"application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}",
|
||||
|
||||
// docs / data
|
||||
"markdown": "mode/markdown/markdown.min.js",
|
||||
"yaml": "mode/yaml/yaml.min.js",
|
||||
"properties": "mode/properties/properties.min.js",
|
||||
"sql": "mode/sql/sql.min.js",
|
||||
"markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}",
|
||||
"yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}",
|
||||
"properties": "mode/properties/properties.min.js?v={{APP_QVER}}",
|
||||
"sql": "mode/sql/sql.min.js?v={{APP_QVER}}",
|
||||
|
||||
// shells
|
||||
"shell": "mode/shell/shell.min.js",
|
||||
"shell": "mode/shell/shell.min.js?v={{APP_QVER}}",
|
||||
|
||||
// languages
|
||||
"python": "mode/python/python.min.js",
|
||||
"text/x-csrc": "mode/clike/clike.min.js",
|
||||
"text/x-c++src": "mode/clike/clike.min.js",
|
||||
"text/x-java": "mode/clike/clike.min.js",
|
||||
"text/x-csharp": "mode/clike/clike.min.js",
|
||||
"text/x-kotlin": "mode/clike/clike.min.js"
|
||||
"python": "mode/python/python.min.js?v={{APP_QVER}}",
|
||||
"text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}"
|
||||
};
|
||||
|
||||
// Map any mime/alias to the key we use in MODE_URL
|
||||
@@ -54,7 +54,7 @@ const MODE_LOAD_TIMEOUT_MS = 2500; // allow closing immediately; don't wait fore
|
||||
function loadScriptOnce(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ver = (window.APP_VERSION ?? 'dev').replace(/^v/, ''); // "v1.6.9" -> "1.6.9"
|
||||
const withQS = url + '?v=' + ver;
|
||||
const withQS = url; //+ '?v=' + ver;
|
||||
|
||||
const key = `cm:${withQS}`;
|
||||
let s = document.querySelector(`script[data-key="${key}"]`);
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
attachEnterKeyListener
|
||||
} from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
} from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}';
|
||||
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||
import {
|
||||
getParentFolder,
|
||||
updateBreadcrumbTitle,
|
||||
@@ -24,13 +24,13 @@ import {
|
||||
hideFolderManagerContextMenu,
|
||||
openRenameFolderModal,
|
||||
openDeleteFolderModal
|
||||
} from './folderManager.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
} from './folderManager.js?v={{APP_QVER}}';
|
||||
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||
import {
|
||||
folderDragOverHandler,
|
||||
folderDragLeaveHandler,
|
||||
folderDropHandler
|
||||
} from './fileDragDrop.js';
|
||||
} from './fileDragDrop.js?v={{APP_QVER}}';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
@@ -750,7 +750,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileEditor.js');
|
||||
const m = await import('./fileEditor.js?v={{APP_QVER}}');
|
||||
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
@@ -759,7 +759,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileActions.js');
|
||||
const m = await import('./fileActions.js?v={{APP_QVER}}');
|
||||
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
@@ -768,7 +768,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./filePreview.js');
|
||||
const m = await import('./filePreview.js?v={{APP_QVER}}');
|
||||
m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
|
||||
});
|
||||
});
|
||||
@@ -822,7 +822,7 @@ function wireSelectAll(fileListContent) {
|
||||
const fileName = this.getAttribute("data-file");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
if (file) {
|
||||
import('./filePreview.js').then(module => {
|
||||
import('./filePreview.js?v={{APP_QVER}}').then(module => {
|
||||
module.openShareModal(file, folder);
|
||||
});
|
||||
}
|
||||
@@ -831,7 +831,7 @@ function wireSelectAll(fileListContent) {
|
||||
updateFileActionButtons();
|
||||
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||||
row.setAttribute("draggable", "true");
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
|
||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||
});
|
||||
});
|
||||
@@ -1085,7 +1085,7 @@ function wireSelectAll(fileListContent) {
|
||||
// preview clicks (dynamic import to avoid global dependency)
|
||||
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
||||
el.addEventListener("click", async () => {
|
||||
const m = await import('./filePreview.js');
|
||||
const m = await import('./filePreview.js?v={{APP_QVER}}');
|
||||
m.previewFile(el.dataset.previewUrl, el.dataset.previewName);
|
||||
});
|
||||
});
|
||||
@@ -1102,7 +1102,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileEditor.js');
|
||||
const m = await import('./fileEditor.js?v={{APP_QVER}}');
|
||||
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
@@ -1111,7 +1111,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileActions.js');
|
||||
const m = await import('./fileActions.js?v={{APP_QVER}}');
|
||||
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
@@ -1123,7 +1123,7 @@ function wireSelectAll(fileListContent) {
|
||||
const fileName = btn.dataset.file;
|
||||
const fileObj = fileData.find(f => f.name === fileName);
|
||||
if (fileObj) {
|
||||
import('./filePreview.js').then(m => m.openShareModal(fileObj, folder));
|
||||
import('./filePreview.js?v={{APP_QVER}}').then(m => m.openShareModal(fileObj, folder));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// fileManager.js
|
||||
import './fileListView.js';
|
||||
import './filePreview.js';
|
||||
import './fileEditor.js';
|
||||
import './fileDragDrop.js';
|
||||
import './fileMenu.js';
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import './fileListView.js?v={{APP_QVER}}';
|
||||
import './filePreview.js?v={{APP_QVER}}';
|
||||
import './fileEditor.js?v={{APP_QVER}}';
|
||||
import './fileDragDrop.js?v={{APP_QVER}}';
|
||||
import './fileMenu.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
|
||||
// Initialize file action buttons.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -14,7 +14,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Attach folder drag-and-drop support for folder tree nodes.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".folder-option").forEach(el => {
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
|
||||
el.addEventListener("dragover", module.folderDragOverHandler);
|
||||
el.addEventListener("dragleave", module.folderDragLeaveHandler);
|
||||
el.addEventListener("drop", module.folderDropHandler);
|
||||
@@ -32,7 +32,7 @@ document.addEventListener("keydown", function(e) {
|
||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
e.preventDefault();
|
||||
import('./fileActions.js').then(module => {
|
||||
import('./fileActions.js?v={{APP_QVER}}').then(module => {
|
||||
module.handleDeleteSelected(new Event("click"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// fileMenu.js
|
||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js';
|
||||
import { previewFile } from './filePreview.js';
|
||||
import { editFile } from './fileEditor.js';
|
||||
import { canEditFile, fileData } from './fileListView.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
import { t } from './i18n.js';
|
||||
import { updateRowHighlight, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { previewFile } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { editFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||
import { canEditFile, fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function showFileContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("fileContextMenu");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// filePreview.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
// Remove any existing modal
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// adding tags to files (with a global tag store for reuse),
|
||||
// updating the file row display with tag badges,
|
||||
// filtering the file list by tag, and persisting tag data.
|
||||
import { escapeHTML } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { renderFileTable, renderGalleryView } from './fileListView.js';
|
||||
import { escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { renderFileTable, renderGalleryView } from './fileListView.js?v={{APP_QVER}}';
|
||||
|
||||
export function openTagModal(file) {
|
||||
// Create the modal element.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// folderManager.js
|
||||
|
||||
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';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||
import { fetchWithCsrf } from './auth.js?v={{APP_QVER}}';
|
||||
import { loadCsrfToken } from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
/* ----------------------
|
||||
Helpers: safe JSON + state
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// js/folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing modal
|
||||
|
||||
@@ -1,55 +1,65 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
import { displayFilePreview } from './filePreview.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js';
|
||||
import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
// /js/main.js
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { editFile, saveFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
// NEW: import shared helpers from appCore (moved out of main.js)
|
||||
import {
|
||||
initializeApp,
|
||||
loadCsrfToken,
|
||||
triggerLogout,
|
||||
setCsrfToken,
|
||||
getCsrfToken
|
||||
} from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
/* =========================
|
||||
CSRF HOTFIX UTILITIES
|
||||
========================= */
|
||||
const _nativeFetch = window.fetch; // keep the real fetch
|
||||
|
||||
function setCsrfToken(token) {
|
||||
if (!token) return;
|
||||
window.csrfToken = token;
|
||||
localStorage.setItem('csrf', token);
|
||||
|
||||
// meta tag for easy access in other places
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.content = token;
|
||||
}
|
||||
function getCsrfToken() {
|
||||
return window.csrfToken || localStorage.getItem('csrf') || '';
|
||||
}
|
||||
// Keep a handle to the native fetch so wrappers never recurse
|
||||
const _nativeFetch = window.fetch.bind(window);
|
||||
|
||||
// Seed CSRF from storage ASAP (before any requests)
|
||||
setCsrfToken(getCsrfToken());
|
||||
|
||||
// Wrap the existing fetchWithCsrf so we also capture rotated tokens from headers.
|
||||
// Wrap fetch so *all* callers get CSRF header + token rotation, without recursion
|
||||
async function fetchWithCsrfAndRefresh(input, init = {}) {
|
||||
const res = await fetchWithCsrf(input, init);
|
||||
const headers = new Headers(init?.headers || {});
|
||||
const token = getCsrfToken();
|
||||
|
||||
if (token && !headers.has('X-CSRF-Token')) {
|
||||
headers.set('X-CSRF-Token', token);
|
||||
}
|
||||
|
||||
const res = await _nativeFetch(input, {
|
||||
credentials: 'include',
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
|
||||
try {
|
||||
const rotated = res.headers?.get('X-CSRF-Token');
|
||||
if (rotated) setCsrfToken(rotated);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Replace global fetch with the wrapped version so *all* callers benefit.
|
||||
window.fetch = fetchWithCsrfAndRefresh;
|
||||
// Avoid double-wrapping if this module re-evaluates for any reason
|
||||
if (!window.fetch || !window.fetch._frWrapped) {
|
||||
const wrapped = fetchWithCsrfAndRefresh;
|
||||
Object.defineProperty(wrapped, '_frWrapped', { value: true });
|
||||
window.fetch = wrapped;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
SAFE API HELPERS
|
||||
@@ -84,6 +94,7 @@ export async function apiPOSTJSON(url, body, opts = {}) {
|
||||
// Optional: expose on window for legacy callers
|
||||
window.apiGETJSON = apiGETJSON;
|
||||
window.apiPOSTJSON = apiPOSTJSON;
|
||||
window.triggerLogout = triggerLogout; // expose the moved helper
|
||||
|
||||
// Global handler to keep UX friendly if something forgets to catch
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
@@ -98,134 +109,16 @@ window.addEventListener("unhandledrejection", (ev) => {
|
||||
});
|
||||
|
||||
/* =========================
|
||||
APP INIT
|
||||
BOOTSTRAP
|
||||
========================= */
|
||||
|
||||
export function initializeApp() {
|
||||
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||
|
||||
//window.currentFolder = "root";
|
||||
const last = localStorage.getItem('lastOpenedFolder');
|
||||
window.currentFolder = last ? last : "root";
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||
loadAdminConfigFunc();
|
||||
initTagSearch();
|
||||
//loadFileList(window.currentFolder);
|
||||
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
if (fileListArea && uploadArea) {
|
||||
fileListArea.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.add('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('dragleave', () => {
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||
dataTransfer: e.dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
// Only run trash/restore for admins
|
||||
const isAdmin =
|
||||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||||
if (isAdmin) {
|
||||
setupTrashRestoreDelete();
|
||||
}
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
if (helpBtn && helpTooltip) {
|
||||
helpBtn.addEventListener("click", () => {
|
||||
helpTooltip.style.display =
|
||||
helpTooltip.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap/refresh CSRF from the server.
|
||||
* Uses the *native* fetch to avoid any wrapper loops and to work even if we don't
|
||||
* yet have a token. Also accepts a rotated token from the response header.
|
||||
*/
|
||||
export function loadCsrfToken() {
|
||||
return _nativeFetch('/api/auth/token.php', { method: 'GET', credentials: 'include' })
|
||||
.then(async res => {
|
||||
// header-based rotation
|
||||
const hdr = res.headers.get('X-CSRF-Token');
|
||||
if (hdr) setCsrfToken(hdr);
|
||||
|
||||
// body (if provided)
|
||||
let body = {};
|
||||
try { body = await res.json(); } catch { /* token endpoint may return empty */ }
|
||||
|
||||
const token = body.csrf_token || getCsrfToken();
|
||||
setCsrfToken(token);
|
||||
|
||||
// share-url meta should reflect the actual origin
|
||||
const actualShare = window.location.origin;
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = actualShare;
|
||||
|
||||
return { csrf_token: token, share_url: actualShare };
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Immediately clear “?logout=1” flag
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
|
||||
export function triggerLogout() {
|
||||
_nativeFetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||||
})
|
||||
.then(() => {
|
||||
document.body.classList.remove('authenticated');
|
||||
window.location.reload(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
window.confirmSingleDownload = confirmSingleDownload;
|
||||
window.openDownloadModal = openDownloadModal;
|
||||
|
||||
// Global variable for the current folder.
|
||||
window.currentFolder = "root";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Load admin config early
|
||||
// Load site config early (safe subset)
|
||||
loadAdminConfigFunc();
|
||||
|
||||
// i18n
|
||||
@@ -242,12 +135,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// 3) If authenticated, start app
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
document.body.classList.add('authenticated');
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.remove();
|
||||
initializeApp();
|
||||
} else {
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -309,4 +199,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
window.scrollBy(0, SCROLL_SPEED);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Expose functions for inline handlers
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
window.confirmSingleDownload = confirmSingleDownload;
|
||||
window.openDownloadModal = openDownloadModal;
|
||||
|
||||
// Global variable for the current folder (initial default; initializeApp will update)
|
||||
window.currentFolder = "root";
|
||||
@@ -1,9 +1,9 @@
|
||||
// trashRestoreDelete.js
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { t } from './i18n.js';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { toggleVisibility, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
function showConfirm(message, onConfirm) {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import { displayFilePreview } from './filePreview.js';
|
||||
import { showToast, escapeHTML } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { showToast, escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Helpers for Drag–and–Drop Folder Uploads (Original Code)
|
||||
|
||||
Reference in New Issue
Block a user