Skip to content

Commit 481580b

Browse files
committed
Themes: Added testing and better mime sniffing for public serving
Existing mime sniffer wasn't great at distinguishing between plaintext file types, so added a custom extension based mapping for common web formats that may be expected to be used with this.
1 parent 593645a commit 481580b

File tree

4 files changed

+45
-5
lines changed

4 files changed

+45
-5
lines changed

app/Http/DownloadResponseFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function streamedFileDirectly(string $filePath, string $fileName, bool $d
7070
public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
7171
{
7272
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
73-
$mime = $rangeStream->sniffMime();
73+
$mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));
7474
$headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
7575

7676
return response()->stream(

app/Http/RangeSupportedStream.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ public function __construct(
3232
/**
3333
* Sniff a mime type from the stream.
3434
*/
35-
public function sniffMime(): string
35+
public function sniffMime(string $extension = ''): string
3636
{
3737
$offset = min(2000, $this->fileSize);
3838
$this->sniffContent = fread($this->stream, $offset);
3939

40-
return (new WebSafeMimeSniffer())->sniff($this->sniffContent);
40+
return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);
4141
}
4242

4343
/**

app/Util/WebSafeMimeSniffer.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class WebSafeMimeSniffer
1313
/**
1414
* @var string[]
1515
*/
16-
protected $safeMimes = [
16+
protected array $safeMimes = [
1717
'application/json',
1818
'application/octet-stream',
1919
'application/pdf',
@@ -48,16 +48,28 @@ class WebSafeMimeSniffer
4848
'video/av1',
4949
];
5050

51+
protected array $textTypesByExtension = [
52+
'css' => 'text/css',
53+
'js' => 'text/javascript',
54+
'json' => 'application/json',
55+
'csv' => 'text/csv',
56+
];
57+
5158
/**
5259
* Sniff the mime-type from the given file content while running the result
5360
* through an allow-list to ensure a web-safe result.
5461
* Takes the content as a reference since the value may be quite large.
62+
* Accepts an optional $extension which can be used for further guessing.
5563
*/
56-
public function sniff(string &$content): string
64+
public function sniff(string &$content, string $extension = ''): string
5765
{
5866
$fInfo = new finfo(FILEINFO_MIME_TYPE);
5967
$mime = $fInfo->buffer($content) ?: 'application/octet-stream';
6068

69+
if ($mime === 'text/plain' && $extension) {
70+
$mime = $this->textTypesByExtension[$extension] ?? 'text/plain';
71+
}
72+
6173
if (in_array($mime, $this->safeMimes)) {
6274
return $mime;
6375
}

tests/ThemeTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,34 @@ public function test_custom_settings_category_page_can_be_added_via_view_file()
464464
});
465465
}
466466

467+
public function test_public_folder_contents_accessible_via_route()
468+
{
469+
$this->usingThemeFolder(function (string $themeFolderName) {
470+
$publicDir = theme_path('public');
471+
mkdir($publicDir, 0777, true);
472+
473+
$text = 'some-text ' . md5(random_bytes(5));
474+
$css = "body { background-color: tomato !important; }";
475+
file_put_contents("{$publicDir}/file.txt", $text);
476+
file_put_contents("{$publicDir}/file.css", $css);
477+
copy($this->files->testFilePath('test-image.png'), "{$publicDir}/image.png");
478+
479+
$resp = $this->asAdmin()->get("/theme/{$themeFolderName}/file.txt");
480+
$resp->assertStreamedContent($text);
481+
$resp->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
482+
$resp->assertHeader('Cache-Control', 'max-age=86400, private');
483+
484+
$resp = $this->asAdmin()->get("/theme/{$themeFolderName}/image.png");
485+
$resp->assertHeader('Content-Type', 'image/png');
486+
$resp->assertHeader('Cache-Control', 'max-age=86400, private');
487+
488+
$resp = $this->asAdmin()->get("/theme/{$themeFolderName}/file.css");
489+
$resp->assertStreamedContent($css);
490+
$resp->assertHeader('Content-Type', 'text/css; charset=UTF-8');
491+
$resp->assertHeader('Cache-Control', 'max-age=86400, private');
492+
});
493+
}
494+
467495
protected function usingThemeFolder(callable $callback)
468496
{
469497
// Create a folder and configure a theme

0 commit comments

Comments
 (0)