Compare commits

..

6 Commits

10 changed files with 269 additions and 136 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## Changes 4/27/2025 1.2.7
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
- Updated `toggleAllCheckboxes(masterCheckbox)` to call `updateRowHighlight()` on each row so selections get the `.row-selected` highlight
- **Master checkbox sync** in toolbar
- Enhanced `updateFileActionButtons()` to set the header checkbox to checked, unchecked, or indeterminate based on how many files are selected
- Fixed Pagination controls & Items-per-page dropdown
- Fixed `#advancedSearchToggle` in both `renderFileTable()` and `renderGalleryView()`
- **Shared folder gallery view logic**
- Introduced new `public/js/sharedFolderView.js` containing all DOMContentLoaded wiring, `toggleViewMode()`, gallery rendering, and event listeners
- Embedded a non-executing JSON payload in `shareFolder.php`
- **`FolderController::shareFolder()` / `shareFolder.php`**
- Removed all inline `onclick="…"` attributes and inline `<script>` blocks
- Added `<script type="application/json" id="shared-data">…</script>` to export `$token` and `$files`
- Added `<script src="/js/sharedFolderView.js" defer></script>` to load the external view logic
- **Styling updates**
- Added `.toggle-btn` CSS for blue header-style toggle button and applied it in JS
- Added `.pagination a:hover { background-color: #0056b3; }` to match button hover
- Tweaked `body` padding and `header h1` margins to reduce whitespace above header
---
## Changes 4/26/2025 1.2.6
**Apache / Dockerfile (CSP)**
@@ -49,6 +71,8 @@
- **Controller**: Updated `FolderController::shareFolder()` (folderController) to include the gallery-view toggle script block intact, ensuring the “Switch to Gallery View” button works when sharing folders.
- **UI (fileListView.js)**: Refactored `renderGalleryView` to remove all inline `onclick=` handlers; switched to using data-attributes and `addEventListener()` for preview, download, edit and rename buttons, fully CSP-compliant.
- Moved logout button handler out of inline `<script>` in `index.html` and into the `DOMContentLoaded` init in **main.js** (via `auth.js`), so it now attaches reliably after the CSRF token is loaded and DOM is ready.
- Added Content-Security-Policy for `<Files "api.php">` block to allow embedding the ReDoc iframe.
- Extracted inline ReDoc init into `public/js/redoc-init.js` and updated `public/api.php` to use deferred `<script>` tags.
---
@@ -208,7 +232,7 @@
Refactored to:
1. Fetch CSRF
2. POST credentials to `/api/auth/auth.php`
3. On `totp_required`, refetch CSRF *again* before calling `openTOTPLoginModal()`
3. On `totp_required`, refetch CSRF again before calling `openTOTPLoginModal()`
4. Handle full logins vs. TOTP flows cleanly.
- **TOTP handlers update**
@@ -1154,7 +1178,7 @@ The enhancements extend the existing drag-and-drop functionality by adding a hea
- Adjusted file preview and icon styling for better alignment.
- Centered the header and optimized the layout for a clean, modern appearance.
*This changelog and feature summary reflect the improvements made during the refactor from a monolithic utils file to modular ES6 components, along with enhancements in UI responsiveness, sorting, file uploads, and file management operations.*
This changelog and feature summary reflect the improvements made during the refactor from a monolithic utils file to modular ES6 components, along with enhancements in UI responsiveness, sorting, file uploads, and file management operations.
---

View File

@@ -121,13 +121,17 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
Require all denied
</FilesMatch>
<Files "api.php">
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.redoc.ly; style-src 'self' 'unsafe-inline'; worker-src 'self' https://cdn.redoc.ly blob:; connect-src 'self'; img-src 'self' data: blob:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
</Files>
ErrorLog /var/www/metadata/log/error.log
CustomLog /var/www/metadata/log/access.log combined
</VirtualHost>
EOF
# Enable required modules
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate ssl
EXPOSE 80 443
COPY start.sh /usr/local/bin/start.sh

View File

@@ -108,16 +108,16 @@ FileRise will be accessible at `http://localhost:8080` (or your servers IP).
If you prefer to run FileRise on a traditional web server (LAMP stack or similar):
- **Requirements:** PHP 8.1 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
- **Requirements:** PHP 8.3 or higher, Apache (with mod_php) or another web server configured for PHP. Ensure PHP extensions json, curl, and zip are enabled. No database needed.
- **Download Files:** Clone this repo or download the [latest release archive](https://github.com/error311/FileRise/releases).
``` bash
git clone https://github.com/error311/FileRise.git
```
Place the files into your web servers directory (e.g., `/var/www/public`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
Place the files into your web servers directory (e.g., `/var/www/`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)
- **Composer Dependencies:** Install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)
- **Folder Permissions:** Ensure the server can write to the following directories (create them if they dont exist):

View File

@@ -19,17 +19,13 @@ if (isset($_GET['spec'])) {
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>FileRise API Docs</title>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
crossorigin="anonymous"></script>
<script defer src="/js/redoc-init.js"></script>
</head>
<body>
<redoc spec-url="api.php?spec=1"></redoc>
<div id="redoc-container"></div>
<script>
if (!customElements.get('redoc')) {
Redoc.init('api.php?spec=1', {}, document.getElementById('redoc-container'));
}
</script>
</body>
</html>

View File

@@ -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.6"; // Update this version string as needed
const version = "v1.2.7"; // Update this version string as needed
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null;

View File

@@ -25,8 +25,9 @@ export function toggleAllCheckboxes(masterCheckbox) {
const checkboxes = document.querySelectorAll(".file-checkbox");
checkboxes.forEach(chk => {
chk.checked = masterCheckbox.checked;
updateRowHighlight(chk);
});
updateFileActionButtons(); // update buttons based on current selection
updateFileActionButtons();
}
export function updateFileActionButtons() {
@@ -38,6 +39,21 @@ export function updateFileActionButtons() {
const zipBtn = document.getElementById("downloadZipBtn");
const extractZipBtn = document.getElementById("extractZipBtn");
// keep the “select all” in sync ——
const master = document.getElementById("selectAll");
if (master) {
if (selectedCheckboxes.length === fileCheckboxes.length) {
master.checked = true;
master.indeterminate = false;
} else if (selectedCheckboxes.length === 0) {
master.checked = false;
master.indeterminate = false;
} else {
master.checked = false;
master.indeterminate = true;
}
}
if (fileCheckboxes.length === 0) {
if (copyBtn) copyBtn.style.display = "none";
if (moveBtn) moveBtn.style.display = "none";
@@ -271,8 +287,6 @@ export function toggleRowSelection(event, fileName) {
const start = Math.min(currentIndex, lastIndex);
const end = Math.max(currentIndex, lastIndex);
// If neither CTRL nor Meta is pressed, you might choose
// to clear existing selections. For this example we leave existing selections intact.
for (let i = start; i <= end; i++) {
const cb = allRows[i].querySelector(".file-checkbox");
if (cb) {

View File

@@ -340,8 +340,48 @@ export function renderFileTable(folder, container) {
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
// pagination clicks
const prevBtn = document.getElementById("prevPageBtn");
if (prevBtn) prevBtn.addEventListener("click", () => {
if (window.currentPage > 1) {
window.currentPage--;
renderFileTable(folder, container);
}
});
const nextBtn = document.getElementById("nextPageBtn");
if (nextBtn) nextBtn.addEventListener("click", () => {
// totalPages is computed above in this scope
if (window.currentPage < totalPages) {
window.currentPage++;
renderFileTable(folder, container);
}
});
// ADD: advanced search toggle
const advToggle = document.getElementById("advancedSearchToggle");
if (advToggle) advToggle.addEventListener("click", () => {
toggleAdvancedSearch();
});
// items-per-page selector
const itemsSelect = document.getElementById("itemsPerPageSelect");
if (itemsSelect) itemsSelect.addEventListener("change", e => {
window.itemsPerPage = parseInt(e.target.value, 10);
localStorage.setItem("itemsPerPage", window.itemsPerPage);
window.currentPage = 1;
renderFileTable(folder, container);
});
// hook up the master checkbox
const selectAll = document.getElementById("selectAll");
if (selectAll) {
selectAll.addEventListener("change", () => {
toggleAllCheckboxes(selectAll);
});
}
// 1) Row-click selects the row
fileListContent.querySelectorAll("tbody tr").forEach(row => {
fileListContent.querySelectorAll("tbody tr").forEach(row => {
row.addEventListener("click", e => {
// grab the underlying checkbox value
const cb = row.querySelector(".file-checkbox");
@@ -575,7 +615,7 @@ export function renderGalleryView(folder, container) {
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
<div class="gallery-preview" style="cursor:pointer;"
data-preview-url="${folderPath+encodeURIComponent(file.name)}?t=${Date.now()}"
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
data-preview-name="${file.name}">
${thumbnail}
</div>
@@ -590,20 +630,20 @@ export function renderGalleryView(folder, container) {
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
<button type="button" class="btn btn-sm btn-success download-btn"
data-download-name="${escapeHTML(file.name)}"
data-download-folder="${file.folder||"root"}"
data-download-folder="${file.folder || "root"}"
title="${t('download')}">
<i class="material-icons">file_download</i>
</button>
${file.editable ? `
<button type="button" class="btn btn-sm edit-btn"
data-edit-name="${escapeHTML(file.name)}"
data-edit-folder="${file.folder||"root"}"
data-edit-folder="${file.folder || "root"}"
title="${t('edit')}">
<i class="material-icons">edit</i>
</button>` : ""}
<button type="button" class="btn btn-sm btn-warning rename-btn"
data-rename-name="${escapeHTML(file.name)}"
data-rename-folder="${file.folder||"root"}"
data-rename-folder="${file.folder || "root"}"
title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
@@ -629,6 +669,40 @@ export function renderGalleryView(folder, container) {
// --- Now wire up all behaviors without inline handlers ---
// ADD: pagination buttons for gallery
const prevBtn = document.getElementById("prevPageBtn");
if (prevBtn) prevBtn.addEventListener("click", () => {
if (window.currentPage > 1) {
window.currentPage--;
renderGalleryView(folder, container);
}
});
const nextBtn = document.getElementById("nextPageBtn");
if (nextBtn) nextBtn.addEventListener("click", () => {
if (window.currentPage < totalPages) {
window.currentPage++;
renderGalleryView(folder, container);
}
});
// ←— ADD: advanced search toggle
const advToggle = document.getElementById("advancedSearchToggle");
if (advToggle) advToggle.addEventListener("click", () => {
toggleAdvancedSearch();
});
// ←— ADD: wire up context-menu in gallery
bindFileListContextMenu();
// ADD: items-per-page selector for gallery
const itemsSelect = document.getElementById("itemsPerPageSelect");
if (itemsSelect) itemsSelect.addEventListener("change", e => {
window.itemsPerPage = parseInt(e.target.value, 10);
localStorage.setItem("itemsPerPage", window.itemsPerPage);
window.currentPage = 1;
renderGalleryView(folder, container);
});
// cache images on load
fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => {
const key = img.dataset.cacheKey;

6
public/js/redoc-init.js Normal file
View File

@@ -0,0 +1,6 @@
// public/js/redoc-init.js
if (!customElements.get('redoc')) {
Redoc.init(window.location.origin + '/api.php?spec=1',
{},
document.getElementById('redoc-container'));
}

View File

@@ -0,0 +1,60 @@
// sharedFolderView.js
document.addEventListener('DOMContentLoaded', function() {
let viewMode = 'list';
const payload = JSON.parse(
document.getElementById('shared-data').textContent
);
const token = payload.token;
const filesData = payload.files;
const downloadBase = `${window.location.origin}/api/folder/downloadSharedFile.php?token=${encodeURIComponent(token)}&file=`;
function toggleViewMode() {
const listEl = document.getElementById('listViewContainer');
const galleryEl = document.getElementById('galleryViewContainer');
const btn = document.getElementById('toggleBtn');
if (btn) {
btn.classList.add('toggle-btn');
}
if (viewMode === 'list') {
viewMode = 'gallery';
listEl.style.display = 'none';
renderGalleryView();
galleryEl.style.display = 'block';
btn.textContent = 'Switch to List View';
} else {
viewMode = 'list';
galleryEl.style.display = 'none';
listEl.style.display = 'block';
btn.textContent = 'Switch to Gallery View';
}
}
document.getElementById('toggleBtn').addEventListener('click', toggleViewMode);
function renderGalleryView() {
const galleryContainer = document.getElementById('galleryViewContainer');
let html = '<div class="shared-gallery-container">';
filesData.forEach(file => {
const url = downloadBase + encodeURIComponent(file);
const ext = file.split('.').pop().toLowerCase();
const thumb = /^(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/.test(ext)
? `<img src="${url}" alt="${file}">`
: `<span class="material-icons">insert_drive_file</span>`;
html += `
<div class="shared-gallery-card">
<div class="gallery-preview" data-url="${url}" style="cursor:pointer;">${thumb}</div>
<div class="gallery-info"><span class="gallery-file-name">${file}</span></div>
</div>`;
});
html += '</div>';
galleryContainer.innerHTML = html;
galleryContainer.querySelectorAll('.gallery-preview')
.forEach(el => el.addEventListener('click', () => {
window.location.href = el.dataset.url;
}));
}
window.renderGalleryView = renderGalleryView;
});

View File

@@ -550,13 +550,18 @@ class FolderController
body {
background: #f2f2f2;
font-family: Arial, sans-serif;
padding: 20px;
padding: 0px 20px 20px 20px;
color: #333;
}
.header {
text-align: center;
margin-bottom: 30px;
margin-top: 0;
}
.header h1 {
margin-top: 0;
}
.container {
@@ -661,6 +666,28 @@ class FolderController
font-size: 0.9rem;
color: #777;
}
.toggle-btn {
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 1rem;
cursor: pointer;
}
.toggle-btn:hover {
background-color: #0056b3;
}
.pagination a:hover {
background-color: #0056b3;
}
.pagination span {
cursor: default;
}
</style>
</head>
@@ -670,7 +697,7 @@ class FolderController
</div>
<div class="container">
<!-- Toggle Button -->
<button id="toggleBtn" class="toggle-btn" onclick="toggleViewMode()">Switch to Gallery View</button>
<button id="toggleBtn" class="toggle-btn">Switch to Gallery View</button>
<!-- List View Container -->
<div id="listViewContainer">
@@ -757,86 +784,14 @@ class FolderController
<div class="footer">
&copy; <?php echo date("Y"); ?> FileRise. All rights reserved.
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// JavaScript for toggling view modes (list/gallery) and wiring up gallery clicks
var viewMode = 'list';
var token = '<?php echo addslashes($token); ?>';
var filesData = <?php echo json_encode($files); ?>;
// Build the download URL base
var downloadBase = window.location.origin +
'/api/folder/downloadSharedFile.php?token=' +
encodeURIComponent(token) +
'&file=';
function toggleViewMode() {
var listEl = document.getElementById('listViewContainer');
var galleryEl = document.getElementById('galleryViewContainer');
var btn = document.getElementById('toggleBtn');
if (viewMode === 'list') {
viewMode = 'gallery';
listEl.style.display = 'none';
renderGalleryView();
galleryEl.style.display = 'block';
btn.textContent = 'Switch to List View';
} else {
viewMode = 'list';
galleryEl.style.display = 'none';
listEl.style.display = 'block';
btn.textContent = 'Switch to Gallery View';
<!-- non-executing JSON payload, never blocked by CSP -->
<script type="application/json" id="shared-data">
{
"token": <?php echo json_encode($token, JSON_HEX_TAG); ?>,
"files": <?php echo json_encode($files, JSON_HEX_TAG); ?>
}
}
// Wire up the toggle button
document.getElementById('toggleBtn')
.addEventListener('click', toggleViewMode);
function renderGalleryView() {
var galleryContainer = document.getElementById('galleryViewContainer');
var html = '<div class="shared-gallery-container">';
filesData.forEach(function(file) {
var encodedName = encodeURIComponent(file);
var fileUrl = downloadBase + encodedName;
var ext = file.split('.').pop().toLowerCase();
var thumb;
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].indexOf(ext) >= 0) {
thumb = '<img src="' + fileUrl + '" alt="' + file + '">';
} else {
thumb = '<span class="material-icons">insert_drive_file</span>';
}
html +=
'<div class="shared-gallery-card">' +
'<div class="gallery-preview" data-url="' + fileUrl + '" style="cursor:pointer;">' +
thumb +
'</div>' +
'<div class="gallery-info">' +
'<span class="gallery-file-name">' + file + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
galleryContainer.innerHTML = html;
// Wire up each thumbnail click
galleryContainer.querySelectorAll('.gallery-preview')
.forEach(function(el) {
el.addEventListener('click', function() {
window.location.href = el.dataset.url;
});
});
}
// Expose for manual invocation if needed
window.renderGalleryView = renderGalleryView;
});
</script>
<script src="/js/sharedFolderView.js" defer></script>
</body>
</html>