Generate real resized thumbnails for image attachments (#98)
Lint / PHP (phpcs PSR-12) (push) Successful in 17s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 20s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m8s
Lint / Deploy (push) Successful in 2s
Lint / PHP (phpcs PSR-12) (push) Successful in 17s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 20s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m8s
Lint / Deploy (push) Successful in 2s
The attachment grid's <img> thumbnail pointed at the same download_attachment.php URL as the full-size original, so previewing a multi-MB photo attachment cost a full multi-MB download just to render a small grid preview. loading="lazy" only deferred off-screen images; it never reduced per-image transfer size. Generate a resized JPEG thumbnail (longest side capped at 300px) via GD at upload time, from the same metadata-stripped image stripImageMetadata() already produces, reusing its decompression-bomb guard (~40MP decode cap). Store the thumbnail's filename in a new nullable ticket_attachments. thumbnail_filename column (migration 005); NULL means no thumbnail exists (non-image, GD unavailable, or an attachment predating this change) and callers fall back to the full-size original. download_attachment.php serves the thumbnail when requested via ?thumb=1 and one exists, falling back to the original otherwise. The attachments grid now requests thumb=1 for its <img> preview; the lightbox link is unchanged and still opens the full-size original. delete_attachment.php removes the thumbnail file alongside the original, and cleanup_orphan_uploads.php's orphan lookup now also matches thumbnail_filename so generated thumbnails aren't swept up as orphans. Verified against real MariaDB + GD: a 1600x1200 test JPEG produced a 300x225 thumbnail at ~1.8KB vs. the 52KB original (~29x smaller); confirmed the serving logic picks the thumbnail for image attachments with one, falls back to the original for a non-image attachment even when thumb=1 is requested, and that the updated orphan-cleanup lookup matches both the original and thumbnail filename (and correctly finds neither for an unrelated filename). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117oBw2jN4kALYeS8HPq4zV
This commit is contained in:
@@ -69,9 +69,21 @@ try {
|
||||
|
||||
$conn->close();
|
||||
|
||||
// Serve the resized preview thumbnail instead of the full-size original
|
||||
// when requested and one was actually generated at upload time; falls
|
||||
// through to the full original otherwise (older attachments predating
|
||||
// thumbnail generation, non-images, or a GD failure at upload time).
|
||||
$wantsThumb = isset($_GET['thumb']) && $_GET['thumb'] === '1';
|
||||
$servedFilename = $attachment['filename'];
|
||||
$servedMimeType = $attachment['mime_type'];
|
||||
if ($wantsThumb && !empty($attachment['thumbnail_filename'])) {
|
||||
$servedFilename = $attachment['thumbnail_filename'];
|
||||
$servedMimeType = 'image/jpeg';
|
||||
}
|
||||
|
||||
// Build file path
|
||||
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads';
|
||||
$filePath = $uploadDir . '/' . $attachment['ticket_id'] . '/' . $attachment['filename'];
|
||||
$filePath = $uploadDir . '/' . $attachment['ticket_id'] . '/' . $servedFilename;
|
||||
|
||||
// Security: Verify the resolved path is within the uploads directory (prevent path traversal)
|
||||
$realUploadDir = realpath($uploadDir);
|
||||
@@ -100,7 +112,7 @@ try {
|
||||
$inlineTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'text/plain'];
|
||||
|
||||
// Set headers
|
||||
$disposition = ($inline && in_array($attachment['mime_type'], $inlineTypes)) ? 'inline' : 'attachment';
|
||||
$disposition = ($inline && in_array($servedMimeType, $inlineTypes)) ? 'inline' : 'attachment';
|
||||
|
||||
// Sanitize filename for Content-Disposition
|
||||
$safeFilename = preg_replace('/[^\w\s\-\.]/', '_', $attachment['original_filename']);
|
||||
@@ -138,7 +150,7 @@ try {
|
||||
$rangeLength = $rangeEnd - $rangeStart + 1;
|
||||
|
||||
header('Accept-Ranges: bytes');
|
||||
header('Content-Type: ' . $attachment['mime_type']);
|
||||
header('Content-Type: ' . $servedMimeType);
|
||||
header('Content-Disposition: ' . $disposition . '; filename="' . $safeFilename . '"');
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
Reference in New Issue
Block a user