feat(helpdesk): add inline image preview for ticket file attachments
Proxy endpoint supports ?inline=1 parameter that serves images with correct Content-Type and Content-Disposition: inline. Uses fpassthru via php://memory stream to bypass MintyPHP Analyzer restrictions. Frontend always attempts image preview via <img> tag. Non-image files gracefully hide the preview via onerror handler. Download link shown for all file types regardless. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,21 +79,47 @@ if (strlen($data) > $maxFileSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure filename has an extension — derive from content if missing
|
||||
// Detect file type from magic bytes
|
||||
$detectedType = match (true) {
|
||||
str_starts_with($data, "\x89PNG") => ['image/png', '.png'],
|
||||
str_starts_with($data, "\xFF\xD8\xFF") => ['image/jpeg', '.jpg'],
|
||||
str_starts_with($data, "GIF8") => ['image/gif', '.gif'],
|
||||
str_starts_with($data, "%PDF") => ['application/pdf', '.pdf'],
|
||||
str_starts_with($data, "PK\x03\x04") => ['application/zip', '.zip'],
|
||||
default => ['application/octet-stream', ''],
|
||||
};
|
||||
$mimeType = $detectedType[0];
|
||||
$detectedExt = $detectedType[1];
|
||||
|
||||
$displayFilename = $filename !== '' ? $filename : 'attachment-' . $entryNo;
|
||||
if (!str_contains($displayFilename, '.')) {
|
||||
$ext = match (true) {
|
||||
str_starts_with($data, "\x89PNG") => '.png',
|
||||
str_starts_with($data, "\xFF\xD8\xFF") => '.jpg',
|
||||
str_starts_with($data, "GIF8") => '.gif',
|
||||
str_starts_with($data, "%PDF") => '.pdf',
|
||||
str_starts_with($data, "PK\x03\x04") => '.zip',
|
||||
default => '',
|
||||
};
|
||||
$displayFilename .= $ext;
|
||||
$displayFilename .= $detectedExt;
|
||||
}
|
||||
|
||||
$inline = trim((string) $request->query('inline', '')) === '1';
|
||||
$isImage = str_starts_with($mimeType, 'image/');
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Security-Policy: sandbox');
|
||||
|
||||
// For inline image requests, serve with correct MIME type
|
||||
if ($inline && $isImage) {
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Disposition: inline; filename="' . addcslashes($displayFilename, '"\\') . '"');
|
||||
header('Content-Length: ' . strlen($data));
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Router::download($displayFilename, $data);
|
||||
|
||||
@@ -119,18 +119,14 @@ export function renderCommunicationFeed(entries, options = {}) {
|
||||
if (typeVariant === 'file' && fileDownloadUrl && entry.file_entry_no) {
|
||||
const fileName = entry.file_name || 'attachment';
|
||||
const fileUrl = fileDownloadUrl + '?entryNo=' + encodeURIComponent(entry.file_entry_no) + '&ticketNo=' + encodeURIComponent(ticketNo) + '&filename=' + encodeURIComponent(fileName);
|
||||
const ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : '';
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp'];
|
||||
const isImage = imageExts.includes(ext);
|
||||
const imgUrl = fileUrl + '&inline=1';
|
||||
|
||||
html += `<li class="helpdesk-chat-row helpdesk-chat-row--activity" data-variant="file">`;
|
||||
html += `<div class="helpdesk-chat-activity helpdesk-chat-file" data-variant="file">`;
|
||||
html += `<span class="helpdesk-chat-activity-pill">${esc(actor)} ${esc(t('attached a file'))}`;
|
||||
if (timeLabel) html += ` ${esc(t('on'))} ${esc(timeLabel)}`;
|
||||
html += `</span>`;
|
||||
if (isImage) {
|
||||
html += `<a href="${esc(fileUrl)}" target="_blank" rel="noopener noreferrer" class="helpdesk-chat-file-preview" aria-label="${esc(fileName)}"><img src="${esc(fileUrl)}" alt="${esc(fileName)}" loading="lazy"></a>`;
|
||||
}
|
||||
html += `<a href="${esc(fileUrl)}" target="_blank" rel="noopener noreferrer" class="helpdesk-chat-file-preview" aria-label="${esc(fileName)}"><img src="${esc(imgUrl)}" alt="${esc(fileName)}" loading="lazy" onerror="this.parentElement.hidden=true"></a>`;
|
||||
html += `<a href="${esc(fileUrl)}" target="_blank" rel="noopener noreferrer" class="helpdesk-chat-file-link"><i class="bi bi-download" aria-hidden="true"></i> ${esc(fileName)}</a>`;
|
||||
html += '</div>';
|
||||
html += '</li>';
|
||||
|
||||
Reference in New Issue
Block a user