Skip to content

Commit 786a434

Browse files
authored
Merge pull request #5405 from BookStackApp/public_theme_files
Theme System: Public serving of files
2 parents b975180 + 25c4f4b commit 786a434

17 files changed

+183
-22
lines changed

app/App/helpers.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use BookStack\App\Model;
4+
use BookStack\Facades\Theme;
45
use BookStack\Permissions\PermissionApplicator;
56
use BookStack\Settings\SettingService;
67
use BookStack\Users\Models\User;
@@ -88,8 +89,7 @@ function setting(string $key = null, $default = null)
8889
*/
8990
function theme_path(string $path = ''): ?string
9091
{
91-
$theme = config('view.theme');
92-
92+
$theme = Theme::getTheme();
9393
if (!$theme) {
9494
return null;
9595
}

app/Exports/Controllers/BookExportController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,6 @@ public function zip(string $bookSlug, ZipExportBuilder $builder)
7676
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
7777
$zip = $builder->buildForBook($book);
7878

79-
return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', filesize($zip), true);
79+
return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true);
8080
}
8181
}

app/Exports/Controllers/ChapterExportController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,6 @@ public function zip(string $bookSlug, string $chapterSlug, ZipExportBuilder $bui
8282
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
8383
$zip = $builder->buildForChapter($chapter);
8484

85-
return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', filesize($zip), true);
85+
return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true);
8686
}
8787
}

app/Exports/Controllers/PageExportController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,6 @@ public function zip(string $bookSlug, string $pageSlug, ZipExportBuilder $builde
8686
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
8787
$zip = $builder->buildForPage($page);
8888

89-
return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', filesize($zip), true);
89+
return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true);
9090
}
9191
}

app/Http/DownloadResponseFactory.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ public function streamedDirectly($stream, string $fileName, int $fileSize): Stre
3939
* Create a response that downloads the given file via a stream.
4040
* Has the option to delete the provided file once the stream is closed.
4141
*/
42-
public function streamedFileDirectly(string $filePath, string $fileName, int $fileSize, bool $deleteAfter = false): StreamedResponse
42+
public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse
4343
{
44+
$fileSize = filesize($filePath);
4445
$stream = fopen($filePath, 'r');
4546

4647
if ($deleteAfter) {
@@ -69,7 +70,7 @@ public function streamedFileDirectly(string $filePath, string $fileName, int $fi
6970
public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
7071
{
7172
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
72-
$mime = $rangeStream->sniffMime();
73+
$mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));
7374
$headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
7475

7576
return response()->stream(
@@ -79,6 +80,22 @@ public function streamedInline($stream, string $fileName, int $fileSize): Stream
7980
);
8081
}
8182

83+
/**
84+
* Create a response that provides the given file via a stream with detected content-type.
85+
* Has the option to delete the provided file once the stream is closed.
86+
*/
87+
public function streamedFileInline(string $filePath, ?string $fileName = null): StreamedResponse
88+
{
89+
$fileSize = filesize($filePath);
90+
$stream = fopen($filePath, 'r');
91+
92+
if ($fileName === null) {
93+
$fileName = basename($filePath);
94+
}
95+
96+
return $this->streamedInline($stream, $fileName, $fileSize);
97+
}
98+
8299
/**
83100
* Get the common headers to provide for a download response.
84101
*/

app/Http/Middleware/PreventResponseCaching.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
class PreventResponseCaching
99
{
10+
/**
11+
* Paths to ignore when preventing response caching.
12+
*/
13+
protected array $ignoredPathPrefixes = [
14+
'theme/',
15+
];
16+
1017
/**
1118
* Handle an incoming request.
1219
*
@@ -20,6 +27,13 @@ public function handle($request, Closure $next)
2027
/** @var Response $response */
2128
$response = $next($request);
2229

30+
$path = $request->path();
31+
foreach ($this->ignoredPathPrefixes as $ignoredPath) {
32+
if (str_starts_with($path, $ignoredPath)) {
33+
return $response;
34+
}
35+
}
36+
2337
$response->headers->set('Cache-Control', 'no-cache, no-store, private');
2438
$response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
2539

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/Theming/ThemeController.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace BookStack\Theming;
4+
5+
use BookStack\Facades\Theme;
6+
use BookStack\Http\Controller;
7+
use BookStack\Util\FilePathNormalizer;
8+
9+
class ThemeController extends Controller
10+
{
11+
/**
12+
* Serve a public file from the configured theme.
13+
*/
14+
public function publicFile(string $theme, string $path)
15+
{
16+
$cleanPath = FilePathNormalizer::normalize($path);
17+
if ($theme !== Theme::getTheme() || !$cleanPath) {
18+
abort(404);
19+
}
20+
21+
$filePath = theme_path("public/{$cleanPath}");
22+
if (!file_exists($filePath)) {
23+
abort(404);
24+
}
25+
26+
$response = $this->download()->streamedFileInline($filePath);
27+
$response->setMaxAge(86400);
28+
29+
return $response;
30+
}
31+
}

app/Theming/ThemeService.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ class ThemeService
1515
*/
1616
protected array $listeners = [];
1717

18+
/**
19+
* Get the currently configured theme.
20+
* Returns an empty string if not configured.
21+
*/
22+
public function getTheme(): string
23+
{
24+
return config('view.theme') ?? '';
25+
}
26+
1827
/**
1928
* Listen to a given custom theme event,
2029
* setting up the action to be ran when the event occurs.

app/Uploads/FileStorage.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
namespace BookStack\Uploads;
44

55
use BookStack\Exceptions\FileUploadException;
6+
use BookStack\Util\FilePathNormalizer;
67
use Exception;
78
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
89
use Illuminate\Filesystem\FilesystemManager;
910
use Illuminate\Support\Facades\Log;
1011
use Illuminate\Support\Str;
11-
use League\Flysystem\WhitespacePathNormalizer;
1212
use Symfony\Component\HttpFoundation\File\UploadedFile;
1313

1414
class FileStorage
@@ -120,12 +120,13 @@ protected function getStorageDiskName(): string
120120
*/
121121
protected function adjustPathForStorageDisk(string $path): string
122122
{
123-
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
123+
$trimmed = str_replace('uploads/files/', '', $path);
124+
$normalized = FilePathNormalizer::normalize($trimmed);
124125

125126
if ($this->getStorageDiskName() === 'local_secure_attachments') {
126-
return $path;
127+
return $normalized;
127128
}
128129

129-
return 'uploads/files/' . $path;
130+
return 'uploads/files/' . $normalized;
130131
}
131132
}

0 commit comments

Comments
 (0)