fix(preview): harden SVG handling and normalize mime type
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 11/29/2025 (v2.2.3)
|
||||||
|
|
||||||
|
fix(preview): harden SVG handling and normalize mime type
|
||||||
|
|
||||||
|
- Stop treating SVGs as inline-previewable images in file list and preview modal
|
||||||
|
- Show a clear “SVG preview disabled for security reasons” message instead
|
||||||
|
- Keep SVGs downloadable via /api/file/download.php with proper image/svg+xml MIME
|
||||||
|
- Add i18n key for svg_preview_disabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 11/29/2025 (v2.2.2)
|
## Changes 11/29/2025 (v2.2.2)
|
||||||
|
|
||||||
release(v2.2.2): feat(folders): show inline folder stats & dates
|
release(v2.2.2): feat(folders): show inline folder stats & dates
|
||||||
|
|||||||
@@ -179,9 +179,22 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||||
|
|
||||||
let previewButton = "";
|
let previewButton = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i.test(file.name)) {
|
|
||||||
|
const isSvg = /\.svg$/i.test(file.name);
|
||||||
|
|
||||||
|
// IMPORTANT: do NOT treat SVG as previewable
|
||||||
|
if (
|
||||||
|
!isSvg &&
|
||||||
|
/\.(jpg|jpeg|png|gif|bmp|webp|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i
|
||||||
|
.test(file.name)
|
||||||
|
) {
|
||||||
let previewIcon = "";
|
let previewIcon = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
|
||||||
|
// images (SVG explicitly excluded)
|
||||||
|
if (
|
||||||
|
/\.(jpg|jpeg|png|gif|bmp|webp|ico|tif|tiff|eps|heic)$/i
|
||||||
|
.test(file.name)
|
||||||
|
) {
|
||||||
previewIcon = `<i class="material-icons">image</i>`;
|
previewIcon = `<i class="material-icons">image</i>`;
|
||||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
|
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">videocam</i>`;
|
previewIcon = `<i class="material-icons">videocam</i>`;
|
||||||
@@ -190,14 +203,16 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||||
}
|
}
|
||||||
previewButton = `<button
|
|
||||||
type="button"
|
previewButton = `
|
||||||
class="btn btn-sm btn-info preview-btn"
|
<button
|
||||||
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
type="button"
|
||||||
data-preview-name="${safeFileName}"
|
class="btn btn-sm btn-info preview-btn"
|
||||||
title="${t('preview')}">
|
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||||
${previewIcon}
|
data-preview-name="${safeFileName}"
|
||||||
</button>`;
|
title="${t('preview')}">
|
||||||
|
${previewIcon}
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -242,13 +257,13 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
<!-- share -->
|
<!-- share -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary btn-sm share-btn ms-1"
|
class="btn btn-secondary btn-sm share-btn ms-1"
|
||||||
data-file="${safeFileName}"
|
data-file="${safeFileName}"
|
||||||
title="${t('share')}">
|
title="${t('share')}">
|
||||||
<i class="material-icons">share</i>
|
<i class="material-icons">share</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1954,7 +1954,7 @@ export function renderGalleryView(folder, container) {
|
|||||||
|
|
||||||
// thumbnail
|
// thumbnail
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
if (/\.(jpe?g|png|gif|bmp|webp|ico)$/i.test(file.name)) {
|
||||||
const cacheKey = previewURL; // include folder & file
|
const cacheKey = previewURL; // include folder & file
|
||||||
if (window.imageCache && window.imageCache[cacheKey]) {
|
if (window.imageCache && window.imageCache[cacheKey]) {
|
||||||
thumbnail = `<img
|
thumbnail = `<img
|
||||||
|
|||||||
@@ -120,7 +120,12 @@ export function openShareModal(file, folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------- Media modal viewer -------------------------------- */
|
/* -------------------------------- Media modal viewer -------------------------------- */
|
||||||
const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i;
|
// Images that are safe to inline in <img> tags:
|
||||||
|
const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|ico)$/i;
|
||||||
|
|
||||||
|
// SVG handled separately so we *don’t* inline it
|
||||||
|
const SVG_RE = /\.svg$/i;
|
||||||
|
|
||||||
const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i;
|
const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i;
|
||||||
const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i;
|
const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i;
|
||||||
const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/i;
|
const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/i;
|
||||||
@@ -422,11 +427,19 @@ export function previewFile(fileUrl, fileName) {
|
|||||||
const folder = window.currentFolder || 'root';
|
const folder = window.currentFolder || 'root';
|
||||||
const name = fileName;
|
const name = fileName;
|
||||||
const lower = (name || '').toLowerCase();
|
const lower = (name || '').toLowerCase();
|
||||||
|
const isSvg = SVG_RE.test(lower);
|
||||||
const isImage = IMG_RE.test(lower);
|
const isImage = IMG_RE.test(lower);
|
||||||
const isVideo = VID_RE.test(lower);
|
const isVideo = VID_RE.test(lower);
|
||||||
const isAudio = AUD_RE.test(lower);
|
const isAudio = AUD_RE.test(lower);
|
||||||
|
|
||||||
setTitle(overlay, name);
|
setTitle(overlay, name);
|
||||||
|
if (isSvg) {
|
||||||
|
container.textContent =
|
||||||
|
t("svg_preview_disabled") ||
|
||||||
|
"SVG preview is disabled for security. Use Download to view this file.";
|
||||||
|
overlay.style.display = "flex";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------- IMAGES -------------------- */
|
/* -------------------- IMAGES -------------------- */
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
|
|||||||
@@ -342,7 +342,8 @@ const translations = {
|
|||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"hide_header_zoom_controls": "Hide header zoom controls",
|
"hide_header_zoom_controls": "Hide header zoom controls",
|
||||||
"preview_not_available": "Preview is not available for this file type.",
|
"preview_not_available": "Preview is not available for this file type.",
|
||||||
"storage_pro_bundle_outdated": "Please upgrade to the latest FileRise Pro bundle to use the Storage explorer."
|
"storage_pro_bundle_outdated": "Please upgrade to the latest FileRise Pro bundle to use the Storage explorer.",
|
||||||
|
"svg_preview_disabled": "SVG preview is disabled for now for security reasons."
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||||
|
|||||||
@@ -503,13 +503,13 @@ class FileModel {
|
|||||||
if (!preg_match(REGEX_FILE_NAME, $file)) {
|
if (!preg_match(REGEX_FILE_NAME, $file)) {
|
||||||
return ["error" => "Invalid file name."];
|
return ["error" => "Invalid file name."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the real upload directory.
|
// Determine the real upload directory.
|
||||||
$uploadDirReal = realpath(UPLOAD_DIR);
|
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||||
if ($uploadDirReal === false) {
|
if ($uploadDirReal === false) {
|
||||||
return ["error" => "Server misconfiguration."];
|
return ["error" => "Server misconfiguration."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine directory based on folder.
|
// Determine directory based on folder.
|
||||||
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||||
$directory = $uploadDirReal;
|
$directory = $uploadDirReal;
|
||||||
@@ -524,11 +524,11 @@ class FileModel {
|
|||||||
return ["error" => "Invalid folder path."];
|
return ["error" => "Invalid folder path."];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the file path.
|
// Build the file path.
|
||||||
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
|
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
|
||||||
$realFilePath = realpath($filePath);
|
$realFilePath = realpath($filePath);
|
||||||
|
|
||||||
// Ensure the file exists and is within the allowed directory.
|
// Ensure the file exists and is within the allowed directory.
|
||||||
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
|
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
|
||||||
return ["error" => "Access forbidden."];
|
return ["error" => "Access forbidden."];
|
||||||
@@ -536,13 +536,19 @@ class FileModel {
|
|||||||
if (!file_exists($realFilePath)) {
|
if (!file_exists($realFilePath)) {
|
||||||
return ["error" => "File not found."];
|
return ["error" => "File not found."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the MIME type with safe fallback.
|
// Get the MIME type with safe fallback.
|
||||||
$mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null;
|
$mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null;
|
||||||
if (!$mimeType) {
|
if (!$mimeType) {
|
||||||
$mimeType = 'application/octet-stream';
|
$mimeType = 'application/octet-stream';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OPTIONAL: normalize SVG MIME
|
||||||
|
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||||
|
if ($ext === 'svg') {
|
||||||
|
$mimeType = 'image/svg+xml';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"filePath" => $realFilePath,
|
"filePath" => $realFilePath,
|
||||||
"mimeType" => $mimeType
|
"mimeType" => $mimeType
|
||||||
|
|||||||
Reference in New Issue
Block a user