33namespace BookStack \Http ;
44
55use BookStack \Util \WebSafeMimeSniffer ;
6- use Symfony \ Component \ HttpFoundation \ HeaderBag ;
6+ use Illuminate \ Http \ Request ;
77
8+ /**
9+ * Helper wrapper for range-based stream response handling.
10+ * Much of this used symfony/http-foundation as a reference during build.
11+ * URL: https://github.com/symfony/http-foundation/blob/v6.0.20/BinaryFileResponse.php
12+ * License: MIT license, Copyright (c) Fabien Potencier.
13+ */
814class RangeSupportedStream
915{
1016 protected string $ sniffContent ;
17+ protected array $ responseHeaders ;
18+ protected int $ responseStatus = 200 ;
19+
20+ protected int $ responseLength = 0 ;
21+ protected int $ responseOffset = 0 ;
1122
1223 public function __construct (
1324 protected $ stream ,
1425 protected int $ fileSize ,
15- protected HeaderBag $ requestHeaders ,
26+ Request $ request ,
1627 ) {
28+ $ this ->responseLength = $ this ->fileSize ;
29+ $ this ->parseRequest ($ request );
1730 }
1831
1932 /**
@@ -40,18 +53,82 @@ public function outputAndClose(): void
4053 }
4154
4255 $ outStream = fopen ('php://output ' , 'w ' );
43- $ offset = 0 ;
56+ $ sniffOffset = strlen ( $ this -> sniffContent ) ;
4457
45- if (!empty ($ this ->sniffContent )) {
46- fwrite ($ outStream , $ this ->sniffContent );
47- $ offset = strlen ($ this ->sniffContent );
58+ if (!empty ($ this ->sniffContent ) && $ this ->responseOffset < $ sniffOffset ) {
59+ $ sniffOutput = substr ($ this ->sniffContent , $ this ->responseOffset , min ($ sniffOffset , $ this ->responseLength ));
60+ fwrite ($ outStream , $ sniffOutput );
61+ } else if ($ this ->responseOffset !== 0 ) {
62+ fseek ($ this ->stream , $ this ->responseOffset );
4863 }
4964
50- $ toWrite = $ this ->fileSize - $ offset ;
51- stream_copy_to_stream ($ this ->stream , $ outStream , $ toWrite );
52- fpassthru ($ this ->stream );
65+ stream_copy_to_stream ($ this ->stream , $ outStream , $ this ->responseLength );
5366
5467 fclose ($ this ->stream );
5568 fclose ($ outStream );
5669 }
70+
71+ public function getResponseHeaders (): array
72+ {
73+ return $ this ->responseHeaders ;
74+ }
75+
76+ public function getResponseStatus (): int
77+ {
78+ return $ this ->responseStatus ;
79+ }
80+
81+ protected function parseRequest (Request $ request ): void
82+ {
83+ $ this ->responseHeaders ['Accept-Ranges ' ] = $ request ->isMethodSafe () ? 'bytes ' : 'none ' ;
84+
85+ $ range = $ this ->getRangeFromRequest ($ request );
86+ if ($ range ) {
87+ [$ start , $ end ] = $ range ;
88+ if ($ start < 0 || $ start > $ end ) {
89+ $ this ->responseStatus = 416 ;
90+ $ this ->responseHeaders ['Content-Range ' ] = sprintf ('bytes */%s ' , $ this ->fileSize );
91+ } elseif ($ end - $ start < $ this ->fileSize - 1 ) {
92+ $ this ->responseLength = $ end < $ this ->fileSize ? $ end - $ start + 1 : -1 ;
93+ $ this ->responseOffset = $ start ;
94+ $ this ->responseStatus = 206 ;
95+ $ this ->responseHeaders ['Content-Range ' ] = sprintf ('bytes %s-%s/%s ' , $ start , $ end , $ this ->fileSize );
96+ $ this ->responseHeaders ['Content-Length ' ] = $ end - $ start + 1 ;
97+ }
98+ }
99+
100+ if ($ request ->isMethod ('HEAD ' )) {
101+ $ this ->responseLength = 0 ;
102+ }
103+ }
104+
105+ protected function getRangeFromRequest (Request $ request ): ?array
106+ {
107+ $ range = $ request ->headers ->get ('Range ' );
108+ if (!$ range || !$ request ->isMethod ('GET ' ) || !str_starts_with ($ range , 'bytes= ' )) {
109+ return null ;
110+ }
111+
112+ if ($ request ->headers ->has ('If-Range ' )) {
113+ return null ;
114+ }
115+
116+ [$ start , $ end ] = explode ('- ' , substr ($ range , 6 ), 2 ) + [0 ];
117+
118+ $ end = ('' === $ end ) ? $ this ->fileSize - 1 : (int ) $ end ;
119+
120+ if ('' === $ start ) {
121+ $ start = $ this ->fileSize - $ end ;
122+ $ end = $ this ->fileSize - 1 ;
123+ } else {
124+ $ start = (int ) $ start ;
125+ }
126+
127+ if ($ start > $ end ) {
128+ return null ;
129+ }
130+
131+ $ end = min ($ end , $ this ->fileSize - 1 );
132+ return [$ start , $ end ];
133+ }
57134}
0 commit comments