-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathRangeSupportedStream.php
More file actions
134 lines (111 loc) · 4.31 KB
/
RangeSupportedStream.php
File metadata and controls
134 lines (111 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
namespace BookStack\Http;
use BookStack\Util\WebSafeMimeSniffer;
use Illuminate\Http\Request;
/**
* Helper wrapper for range-based stream response handling.
* Much of this used symfony/http-foundation as a reference during build.
* URL: https://github.com/symfony/http-foundation/blob/v6.0.20/BinaryFileResponse.php
* License: MIT license, Copyright (c) Fabien Potencier.
*/
class RangeSupportedStream
{
protected string $sniffContent = '';
protected array $responseHeaders = [];
protected int $responseStatus = 200;
protected int $responseLength = 0;
protected int $responseOffset = 0;
public function __construct(
protected $stream,
protected int $fileSize,
Request $request,
) {
$this->responseLength = $this->fileSize;
$this->parseRequest($request);
}
/**
* Sniff a mime type from the stream.
*/
public function sniffMime(string $extension = ''): string
{
$offset = min(2000, $this->fileSize);
$this->sniffContent = fread($this->stream, $offset);
return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);
}
/**
* Output the current stream to stdout before closing out the stream.
*/
public function outputAndClose(): void
{
// End & flush the output buffer, if we're in one, otherwise we still use memory.
// Output buffer may or may not exist depending on PHP `output_buffering` setting.
// Ignore in testing since output buffers are used to gather a response.
if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
ob_end_clean();
}
$outStream = fopen('php://output', 'w');
$sniffLength = strlen($this->sniffContent);
$bytesToWrite = $this->responseLength;
if ($sniffLength > 0 && $this->responseOffset < $sniffLength) {
$sniffEnd = min($sniffLength, $bytesToWrite + $this->responseOffset);
$sniffOutLength = $sniffEnd - $this->responseOffset;
$sniffOutput = substr($this->sniffContent, $this->responseOffset, $sniffOutLength);
fwrite($outStream, $sniffOutput);
$bytesToWrite -= $sniffOutLength;
} else if ($this->responseOffset !== 0) {
fseek($this->stream, $this->responseOffset);
}
stream_copy_to_stream($this->stream, $outStream, $bytesToWrite);
fclose($this->stream);
fclose($outStream);
}
public function getResponseHeaders(): array
{
return $this->responseHeaders;
}
public function getResponseStatus(): int
{
return $this->responseStatus;
}
protected function parseRequest(Request $request): void
{
$this->responseHeaders['Accept-Ranges'] = $request->isMethodSafe() ? 'bytes' : 'none';
$range = $this->getRangeFromRequest($request);
if ($range) {
[$start, $end] = $range;
if ($start < 0 || $start > $end) {
$this->responseStatus = 416;
$this->responseHeaders['Content-Range'] = sprintf('bytes */%s', $this->fileSize);
} else {
$this->responseLength = $end < $this->fileSize ? $end - $start + 1 : -1;
$this->responseOffset = $start;
$this->responseStatus = 206;
$this->responseHeaders['Content-Range'] = sprintf('bytes %s-%s/%s', $start, $end, $this->fileSize);
$this->responseHeaders['Content-Length'] = $end - $start + 1;
}
}
if ($request->isMethod('HEAD')) {
$this->responseLength = 0;
}
}
protected function getRangeFromRequest(Request $request): ?array
{
$range = $request->headers->get('Range');
if (!$range || !$request->isMethod('GET') || !str_starts_with($range, 'bytes=')) {
return null;
}
if ($request->headers->has('If-Range')) {
return null;
}
[$start, $end] = explode('-', substr($range, 6), 2) + [0];
$end = ('' === $end) ? $this->fileSize - 1 : (int) $end;
if ('' === $start) {
$start = $this->fileSize - $end;
$end = $this->fileSize - 1;
} else {
$start = (int) $start;
}
$end = min($end, $this->fileSize - 1);
return [$start, $end];
}
}