public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
{
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
- $mime = $rangeStream->sniffMime();
+ $mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));
$headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
return response()->stream(
/**
* Sniff a mime type from the stream.
*/
- public function sniffMime(): string
+ public function sniffMime(string $extension = ''): string
{
$offset = min(2000, $this->fileSize);
$this->sniffContent = fread($this->stream, $offset);
- return (new WebSafeMimeSniffer())->sniff($this->sniffContent);
+ return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);
}
/**
/**
* @var string[]
*/
- protected $safeMimes = [
+ protected array $safeMimes = [
'application/json',
'application/octet-stream',
'application/pdf',
'video/av1',
];
+ protected array $textTypesByExtension = [
+ 'css' => 'text/css',
+ 'js' => 'text/javascript',
+ 'json' => 'application/json',
+ 'csv' => 'text/csv',
+ ];
+
/**
* Sniff the mime-type from the given file content while running the result
* through an allow-list to ensure a web-safe result.
* Takes the content as a reference since the value may be quite large.
+ * Accepts an optional $extension which can be used for further guessing.
*/
- public function sniff(string &$content): string
+ public function sniff(string &$content, string $extension = ''): string
{
$fInfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $fInfo->buffer($content) ?: 'application/octet-stream';
+ if ($mime === 'text/plain' && $extension) {
+ $mime = $this->textTypesByExtension[$extension] ?? 'text/plain';
+ }
+
if (in_array($mime, $this->safeMimes)) {
return $mime;
}
});
}
+ public function test_public_folder_contents_accessible_via_route()
+ {
+ $this->usingThemeFolder(function (string $themeFolderName) {
+ $publicDir = theme_path('public');
+ mkdir($publicDir, 0777, true);
+
+ $text = 'some-text ' . md5(random_bytes(5));
+ $css = "body { background-color: tomato !important; }";
+ file_put_contents("{$publicDir}/file.txt", $text);
+ file_put_contents("{$publicDir}/file.css", $css);
+ copy($this->files->testFilePath('test-image.png'), "{$publicDir}/image.png");
+
+ $resp = $this->asAdmin()->get("/theme/{$themeFolderName}/file.txt");
+ $resp->assertStreamedContent($text);
+ $resp->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
+ $resp->assertHeader('Cache-Control', 'max-age=86400, private');
+
+ $resp = $this->asAdmin()->get("/theme/{$themeFolderName}/image.png");
+ $resp->assertHeader('Content-Type', 'image/png');
+ $resp->assertHeader('Cache-Control', 'max-age=86400, private');
+
+ $resp = $this->asAdmin()->get("/theme/{$themeFolderName}/file.css");
+ $resp->assertStreamedContent($css);
+ $resp->assertHeader('Content-Type', 'text/css; charset=UTF-8');
+ $resp->assertHeader('Cache-Control', 'max-age=86400, private');
+ });
+ }
+
protected function usingThemeFolder(callable $callback)
{
// Create a folder and configure a theme