1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-01-31 12:11:37 +01:00

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.
This commit is contained in:
Dan Brown 2025-01-13 16:51:07 +00:00
parent 593645acfe
commit 481580be17
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 45 additions and 5 deletions

View File

@ -70,7 +70,7 @@ class DownloadResponseFactory
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(

View File

@ -32,12 +32,12 @@ class RangeSupportedStream
/**
* 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);
}
/**

View File

@ -13,7 +13,7 @@ class WebSafeMimeSniffer
/**
* @var string[]
*/
protected $safeMimes = [
protected array $safeMimes = [
'application/json',
'application/octet-stream',
'application/pdf',
@ -48,16 +48,28 @@ class WebSafeMimeSniffer
'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;
}

View File

@ -464,6 +464,34 @@ END;
});
}
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