getFileVisibility($requestedPath); if ($accessTypeId !== null && $accessTypeId === 3) { http_response_code(403); exit; } } catch (\Throwable $e) { ErrorHandler::log('media_visibility', $e, ['path' => $path]); } } // 4. Verify MIME type $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($realFull); // finfo may return 'text/plain' for WebVTT files on some systems; // re-classify by extension so we don't block them. $ext = strtolower(pathinfo($realFull, PATHINFO_EXTENSION)); if ($mimeType === 'text/plain' && $ext === 'vtt') { $mimeType = 'text/vtt'; } // finfo may return application/octet-stream for valid downloadable files // that have known extensions — allow them through. $knownDownloadExts = ['zip','tar','gz','tgz','mp3','ogg','oga','wav','flac','aac','m4a', 'webm','ogv','mov','gif','webp','pdf','vtt']; $allowedMimes = [ // Images 'image/jpeg', 'image/png', 'image/gif', 'image/webp', // Documents 'application/pdf', // Video 'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', // Audio 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav', 'audio/flac', 'audio/aac', 'audio/x-m4a', 'audio/mp4', // Captions 'text/vtt', // Archives 'application/zip', 'application/x-zip-compressed', 'application/x-tar', 'application/gzip', // Generic binary (allowed when ext is known) 'application/octet-stream', ]; $isAllowed = in_array($mimeType, $allowedMimes, true) || in_array($ext, $knownDownloadExts, true); if (!$isAllowed) { http_response_code(403); exit; } // 5. Determine if download was explicitly requested $forceDownload = !empty($_GET['download']) && $_GET['download'] === '1'; // 6. Send response headers header('Content-Type: ' . $mimeType); header('Content-Length: ' . (int) filesize($realFull)); header('X-Content-Type-Options: nosniff'); if ($ext === 'vtt') { header('Content-Type: text/vtt; charset=utf-8'); header('Cache-Control: public, max-age=86400'); } elseif (in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) { header('Cache-Control: public, max-age=604800'); if (!$forceDownload) { header('Content-Disposition: inline'); } } elseif ($ext === 'pdf') { header('Cache-Control: public, max-age=86400'); header('Content-Disposition: ' . ($forceDownload ? 'attachment' : 'inline')); } elseif (in_array($ext, ['mp4','webm','ogv','mov'], true)) { // Video: no cache-control range requests should work header('Accept-Ranges: bytes'); header('Cache-Control: public, max-age=86400'); } elseif (in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) { header('Accept-Ranges: bytes'); header('Cache-Control: public, max-age=86400'); } else { // Unknown / other: force download $safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', basename($realFull)); header('Content-Disposition: attachment; filename="' . $safeFilename . '"'); header('Cache-Control: private, no-store'); } // 7. Stream file (with range support for media) if (in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a'], true)) { $this->streamWithRange($realFull, $mimeType); } else { readfile($realFull); } } /** * Stream a file with HTTP Range support (required for HTML5 audio/video seeking). */ private function streamWithRange(string $path, string $mimeType): void { $size = (int) filesize($path); $start = 0; $end = $size - 1; if (isset($_SERVER['HTTP_RANGE'])) { $range = $_SERVER['HTTP_RANGE']; if (!preg_match('/bytes=\d*-\d*/', $range)) { header('HTTP/1.1 416 Range Not Satisfiable'); header('Content-Range: bytes */' . $size); exit; } [, $range] = explode('=', $range, 2); [$start, $end] = array_pad(explode('-', $range, 2), 2, ''); $start = ($start === '') ? 0 : (int)$start; $end = ($end === '') ? $size - 1 : (int)$end; if ($end >= $size) { $end = $size - 1; } if ($start > $end) { http_response_code(416); exit; } header('HTTP/1.1 206 Partial Content'); header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); header('Content-Length: ' . ($end - $start + 1)); } else { header('Content-Length: ' . $size); } $fp = fopen($path, 'rb'); if ($fp === false) { http_response_code(500); exit; } fseek($fp, $start); $remaining = $end - $start + 1; while ($remaining > 0 && !feof($fp)) { $chunk = fread($fp, min(8192, $remaining)); if ($chunk === false) { break; } echo $chunk; $remaining -= strlen($chunk); } fclose($fp); } }