mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
From testing, don't think this could exploited directly, as the response would error instead of allowing control characters, but this adds an extra layer of sanitization, and switches to encoded disposition filenames for better UTF8 support.
118 lines
4.1 KiB
PHP
118 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace BookStack\Http;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class DownloadResponseFactory
|
|
{
|
|
public function __construct(
|
|
protected Request $request,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Create a response that directly forces a download in the browser.
|
|
*/
|
|
public function directly(string $content, string $fileName): Response
|
|
{
|
|
return response()->make($content, 200, $this->getHeaders($fileName, strlen($content)));
|
|
}
|
|
|
|
/**
|
|
* Create a response that forces a download, from a given stream of content.
|
|
*/
|
|
public function streamedDirectly($stream, string $fileName, int $fileSize): StreamedResponse
|
|
{
|
|
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
|
|
$headers = array_merge($this->getHeaders($fileName, $fileSize), $rangeStream->getResponseHeaders());
|
|
return response()->stream(
|
|
fn() => $rangeStream->outputAndClose(),
|
|
$rangeStream->getResponseStatus(),
|
|
$headers,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a response that downloads the given file via a stream.
|
|
* Has the option to delete the provided file once the stream is closed.
|
|
*/
|
|
public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse
|
|
{
|
|
$fileSize = filesize($filePath);
|
|
$stream = fopen($filePath, 'r');
|
|
|
|
if ($deleteAfter) {
|
|
// Delete the given file if it still exists after the app terminates
|
|
$callback = function () use ($filePath) {
|
|
if (file_exists($filePath)) {
|
|
unlink($filePath);
|
|
}
|
|
};
|
|
|
|
// We watch both app terminate and php shutdown to cover both normal app termination
|
|
// as well as other potential scenarios (connection termination).
|
|
app()->terminating($callback);
|
|
register_shutdown_function($callback);
|
|
}
|
|
|
|
return $this->streamedDirectly($stream, $fileName, $fileSize);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a file download response that provides the file with a content-type
|
|
* correct for the file, in a way so the browser can show the content in browser,
|
|
* for a given content stream.
|
|
*/
|
|
public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
|
|
{
|
|
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
|
|
$mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));
|
|
$headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
|
|
|
|
return response()->stream(
|
|
fn() => $rangeStream->outputAndClose(),
|
|
$rangeStream->getResponseStatus(),
|
|
$headers,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a response that provides the given file via a stream with detected content-type.
|
|
* Has the option to delete the provided file once the stream is closed.
|
|
*/
|
|
public function streamedFileInline(string $filePath, ?string $fileName = null): StreamedResponse
|
|
{
|
|
$fileSize = filesize($filePath);
|
|
$stream = fopen($filePath, 'r');
|
|
|
|
if ($fileName === null) {
|
|
$fileName = basename($filePath);
|
|
}
|
|
|
|
return $this->streamedInline($stream, $fileName, $fileSize);
|
|
}
|
|
|
|
/**
|
|
* Get the common headers to provide for a download response.
|
|
*/
|
|
protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array
|
|
{
|
|
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
|
|
|
|
$downloadName = str_replace(['"', '/', '\\', '$'], '', $fileName);
|
|
$downloadName = preg_replace('/[\x00-\x1F\x7F]/', '', $downloadName);
|
|
$encodedDownloadName = rawurlencode($downloadName);
|
|
|
|
return [
|
|
'Content-Type' => $mime,
|
|
'Content-Length' => $fileSize,
|
|
'Content-Disposition' => "{$disposition}; filename*=UTF-8''{$encodedDownloadName}",
|
|
'X-Content-Type-Options' => 'nosniff',
|
|
];
|
|
}
|
|
}
|