This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
$loggedin = isset($_SESSION['neptune_loggedin']) && $_SESSION['neptune_loggedin'] === true;
|
||||
if (!$loggedin) {
|
||||
http_response_code(401);
|
||||
echo 'Unauthorized';
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_GET['file'] ?? '';
|
||||
$mode = $_GET['mode'] ?? 'download';
|
||||
|
||||
if (!$file || preg_match('/[^a-zA-Z0-9_\.\-]/', $file)) {
|
||||
http_response_code(400);
|
||||
echo 'Invalid file';
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = '/var/www/uploads/' . basename($file);
|
||||
if (!file_exists($path)) {
|
||||
http_response_code(404);
|
||||
echo 'File not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/config/database.php';
|
||||
$db = getDbConnection();
|
||||
$stmt = $db->prepare("SELECT original_name, mime_type FROM file_attachments WHERE stored_name = ?");
|
||||
$stmt->execute([basename($file)]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$originalName = $att ? $att['original_name'] : basename($file);
|
||||
$mimeType = $att && $att['mime_type'] ? $att['mime_type'] : mime_content_type($path);
|
||||
|
||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
$viewable = in_array($ext, ['txt', 'md', 'pdf', 'csv']);
|
||||
|
||||
if ($mode === 'view' && $viewable) {
|
||||
header('Content-Disposition: inline; filename="' . $originalName . '"');
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-File-Name: ' . $originalName);
|
||||
header('X-File-Viewable: 1');
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $originalName . '"');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Cache-Control: no-cache');
|
||||
readfile($path);
|
||||
+9
-1
@@ -16,7 +16,15 @@ server {
|
||||
|
||||
location /uploads/ {
|
||||
alias /var/www/uploads/;
|
||||
add_header Content-Disposition 'inline';
|
||||
internal;
|
||||
}
|
||||
|
||||
location /download/ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/backend/download.php;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param QUERY_STRING $query_string;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
@@ -283,3 +283,14 @@ kbd {
|
||||
color: #60a5fa;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* File Viewer */
|
||||
#fileViewerModal .modal-content {
|
||||
border-color: var(--neptune-border);
|
||||
}
|
||||
|
||||
#fileViewerBody pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -588,13 +588,20 @@ async function renderAttachments(eventId, container) {
|
||||
container.innerHTML = attachments.map(a => {
|
||||
const icon = getFileIcon(a.mime_type, a.original_name);
|
||||
const size = formatFileSize(a.file_size);
|
||||
const ext = a.original_name.split('.').pop().toLowerCase();
|
||||
const viewable = ['txt', 'md', 'csv'].includes(ext);
|
||||
const href = viewable ? '#' : '/download/?file=' + encodeURIComponent(a.stored_name) + '&mode=download';
|
||||
const onclick = viewable ? ` onclick="event.preventDefault();openFileViewer('${esc(a.stored_name)}','${esc(a.original_name)}')"` : '';
|
||||
return `<div class="attachment-item d-flex align-items-center justify-content-between py-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas ${icon} me-2" style="font-size:.85rem;"></i>
|
||||
<a href="/uploads/${a.stored_name}" target="_blank" class="small text-decoration-none" style="word-break:break-all;">${esc(a.original_name)}</a>
|
||||
<small class="text-secondary ms-2">(${size})</small>
|
||||
<div class="d-flex align-items-center" style="min-width:0;">
|
||||
<i class="fas ${icon} me-2" style="font-size:.85rem;flex-shrink:0;"></i>
|
||||
<a href="${href}"${onclick} target="_blank" class="small text-decoration-none text-truncate" style="color:var(--neptune-accent);">${esc(a.original_name)}</a>
|
||||
<small class="text-secondary ms-2 flex-shrink-0">(${size})</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 flex-shrink-0">
|
||||
<a href="/download/?file=${encodeURIComponent(a.stored_name)}&mode=download" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download" style="font-size:.6rem;"><i class="fas fa-download"></i></a>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteAttachment(${a.id}, this)" title="Delete file" style="font-size:.6rem;"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1 ms-2" onclick="deleteAttachment(${a.id}, this)" title="Delete file" style="font-size:.65rem;"><i class="fas fa-times"></i></button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -618,6 +625,41 @@ function getFileIcon(mime, name) {
|
||||
return 'fa-file';
|
||||
}
|
||||
|
||||
function openFileViewer(storedName, originalName) {
|
||||
const modalEl = document.getElementById('fileViewerModal');
|
||||
document.getElementById('fileViewerName').textContent = originalName;
|
||||
document.getElementById('fileViewerDownloadBtn').onclick = () => {
|
||||
window.open('/download/?file=' + encodeURIComponent(storedName) + '&mode=download', '_blank');
|
||||
};
|
||||
const body = document.getElementById('fileViewerBody');
|
||||
body.innerHTML = '<div class="text-center text-secondary py-5"><div class="spinner-border" role="status"></div><p class="mt-2 small">Loading file...</p></div>';
|
||||
|
||||
const modal = new bootstrap.Modal(modalEl);
|
||||
modal.show();
|
||||
|
||||
const ext = originalName.split('.').pop().toLowerCase();
|
||||
if (ext === 'pdf') {
|
||||
body.innerHTML = `<iframe src="/download/?file=${encodeURIComponent(storedName)}&mode=view" style="width:100%;min-height:80vh;border:none;"></iframe>`;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/download/?file=' + encodeURIComponent(storedName) + '&mode=view')
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('Failed to load');
|
||||
return r.text();
|
||||
})
|
||||
.then(text => {
|
||||
if (ext === 'md') {
|
||||
body.innerHTML = '<div class="p-3" style="white-space:pre-wrap;font-family:var(--bs-font-monospace);font-size:.85rem;line-height:1.6;color:#e2e8f0;">' + esc(text) + '</div>';
|
||||
} else {
|
||||
body.innerHTML = '<div class="p-3" style="white-space:pre-wrap;font-family:var(--bs-font-monospace);font-size:.85rem;line-height:1.6;color:#e2e8f0;">' + esc(text) + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
body.innerHTML = '<div class="text-center text-danger py-5"><i class="fas fa-exclamation-circle fs-2 mb-2"></i><p class="small">Failed to load file</p></div>';
|
||||
});
|
||||
}
|
||||
|
||||
async function saveEvent() {
|
||||
const tags = parseEventTags();
|
||||
const data = {
|
||||
|
||||
@@ -527,6 +527,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== FILE VIEWER MODAL ==================== -->
|
||||
<div class="modal fade" id="fileViewerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header border-secondary py-2">
|
||||
<h6 class="modal-title" id="fileViewerTitle"><i class="fas fa-file me-1"></i> <span id="fileViewerName"></span></h6>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="fileViewerDownloadBtn"><i class="fas fa-download me-1"></i>Download</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body p-0" id="fileViewerBody" style="min-height:60vh;max-height:80vh;overflow:auto;background:#0d1117;">
|
||||
<div class="text-center text-secondary py-5"><div class="spinner-border" role="status"></div><p class="mt-2 small">Loading file...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== LOGIN OVERLAY ==================== -->
|
||||
<div id="loginOverlay" style="position:fixed;top:0;left:0;width:100%;height:100%;background:#0a0e1a;z-index:9999;display:flex;align-items:center;justify-content:center;">
|
||||
<div style="text-align:center;max-width:400px;padding:2rem;">
|
||||
|
||||
Reference in New Issue
Block a user