1212 * that resembles PHP's `$_POST` and `$_FILES` superglobals.
1313 *
1414 * @internal
15+ * @link https://tools.ietf.org/html/rfc7578
16+ * @link https://tools.ietf.org/html/rfc2046#section-5.1.1
1517 */
1618final class MultipartParser
1719{
18- /**
19- * @var string
20- */
21- protected $ buffer = '' ;
22-
23- /**
24- * @var string
25- */
26- protected $ boundary ;
27-
2820 /**
2921 * @var ServerRequestInterface
3022 */
3123 protected $ request ;
3224
33- /**
34- * @var HttpBodyStream
35- */
36- protected $ body ;
37-
38- /**
39- * @var callable
40- */
41- protected $ onDataCallable ;
42-
4325 /**
4426 * @var int|null
4527 */
@@ -58,138 +40,127 @@ private function __construct(ServerRequestInterface $request)
5840
5941 private function parse ()
6042 {
61- $ this ->buffer = (string )$ this ->request ->getBody ();
62-
63- $ this ->determineStartMethod ();
64-
65- return $ this ->request ;
66- }
67-
68- private function determineStartMethod ()
69- {
70- if (!$ this ->request ->hasHeader ('content-type ' )) {
71- $ this ->findBoundary ();
72- return ;
73- }
74-
7543 $ contentType = $ this ->request ->getHeaderLine ('content-type ' );
76- preg_match ('/boundary="?(.*)"?$/ ' , $ contentType , $ matches );
77- if (isset ($ matches [1 ])) {
78- $ this ->boundary = $ matches [1 ];
79- $ this ->parseBuffer ();
80- return ;
44+ if (!preg_match ('/boundary="?(.*)"?$/ ' , $ contentType , $ matches )) {
45+ return $ this ->request ;
8146 }
8247
83- $ this ->findBoundary ();
84- }
48+ $ this ->parseBody ('-- ' . $ matches [1 ], (string )$ this ->request ->getBody ());
8549
86- private function findBoundary ()
87- {
88- if (substr ($ this ->buffer , 0 , 3 ) === '--- ' && strpos ($ this ->buffer , "\r\n" ) !== false ) {
89- $ boundary = substr ($ this ->buffer , 2 , strpos ($ this ->buffer , "\r\n" ));
90- $ boundary = substr ($ boundary , 0 , -2 );
91- $ this ->boundary = $ boundary ;
92- $ this ->parseBuffer ();
93- }
50+ return $ this ->request ;
9451 }
9552
96- private function parseBuffer ( )
53+ private function parseBody ( $ boundary , $ buffer )
9754 {
98- $ chunks = explode ('-- ' . $ this ->boundary , $ this ->buffer );
99- $ this ->buffer = array_pop ($ chunks );
100- foreach ($ chunks as $ chunk ) {
101- $ chunk = $ this ->stripTrailingEOL ($ chunk );
102- $ this ->parseChunk ($ chunk );
55+ $ len = strlen ($ boundary );
56+
57+ // ignore everything before initial boundary (SHOULD be empty)
58+ $ start = strpos ($ buffer , $ boundary . "\r\n" );
59+
60+ while ($ start !== false ) {
61+ // search following boundary (preceded by newline)
62+ // ignore last if not followed by boundary (SHOULD end with "--")
63+ $ start += $ len + 2 ;
64+ $ end = strpos ($ buffer , "\r\n" . $ boundary , $ start );
65+ if ($ end === false ) {
66+ break ;
67+ }
68+
69+ // parse one part and continue searching for next
70+ $ this ->parsePart (substr ($ buffer , $ start , $ end - $ start ));
71+ $ start = $ end ;
10372 }
10473 }
10574
106- private function parseChunk ($ chunk )
75+ private function parsePart ($ chunk )
10776 {
108- if ($ chunk === '' ) {
77+ $ pos = strpos ($ chunk , "\r\n\r\n" );
78+ if ($ pos === false ) {
10979 return ;
11080 }
11181
112- list ( $ header , $ body ) = explode ( "\r\n\r\n" , $ chunk , 2 );
113- $ headers = $ this -> parseHeaders ( $ header );
82+ $ headers = $ this -> parseHeaders (( string ) substr ( $ chunk , 0 , $ pos ) );
83+ $ body = ( string ) substr ( $ chunk , $ pos + 4 );
11484
11585 if (!isset ($ headers ['content-disposition ' ])) {
11686 return ;
11787 }
11888
119- if (!$ this ->headerContainsParameter ($ headers ['content-disposition ' ], 'name ' )) {
89+ $ name = $ this ->getParameterFromHeader ($ headers ['content-disposition ' ], 'name ' );
90+ if ($ name === null ) {
12091 return ;
12192 }
12293
123- if ($ this ->headerContainsParameter ($ headers ['content-disposition ' ], 'filename ' )) {
124- $ this ->parseFile ($ headers , $ body );
94+ $ filename = $ this ->getParameterFromHeader ($ headers ['content-disposition ' ], 'filename ' );
95+ if ($ filename !== null ) {
96+ $ this ->parseFile (
97+ $ name ,
98+ $ filename ,
99+ isset ($ headers ['content-type ' ][0 ]) ? $ headers ['content-type ' ][0 ] : null ,
100+ $ body
101+ );
125102 } else {
126- $ this ->parsePost ($ headers , $ body );
103+ $ this ->parsePost ($ name , $ body );
127104 }
128105 }
129106
130- private function parseFile ($ headers , $ body )
107+ private function parseFile ($ name , $ filename , $ contentType , $ contents )
131108 {
132109 $ this ->request = $ this ->request ->withUploadedFiles ($ this ->extractPost (
133110 $ this ->request ->getUploadedFiles (),
134- $ this -> getParameterFromHeader ( $ headers [ ' content-disposition ' ], ' name ' ) ,
135- $ this ->parseUploadedFile ($ headers , $ body )
111+ $ name ,
112+ $ this ->parseUploadedFile ($ filename , $ contentType , $ contents )
136113 ));
137114 }
138115
139- private function parseUploadedFile ($ headers , $ body )
116+ private function parseUploadedFile ($ filename , $ contentType , $ contents )
140117 {
141- $ filename = $ this ->getParameterFromHeader ($ headers ['content-disposition ' ], 'filename ' );
142- $ bodyLength = strlen ($ body );
118+ $ size = strlen ($ contents );
143119
144120 // no file selected (zero size and empty filename)
145- if ($ bodyLength === 0 && $ filename === '' ) {
121+ if ($ size === 0 && $ filename === '' ) {
146122 return new UploadedFile (
147123 Psr7 \stream_for ('' ),
148- $ bodyLength ,
124+ $ size ,
149125 UPLOAD_ERR_NO_FILE ,
150126 $ filename ,
151- $ headers [ ' content-type ' ][ 0 ]
127+ $ contentType
152128 );
153129 }
154130
155131 // file exceeds MAX_FILE_SIZE value
156- if ($ this ->maxFileSize !== null && $ bodyLength > $ this ->maxFileSize ) {
132+ if ($ this ->maxFileSize !== null && $ size > $ this ->maxFileSize ) {
157133 return new UploadedFile (
158134 Psr7 \stream_for ('' ),
159- $ bodyLength ,
135+ $ size ,
160136 UPLOAD_ERR_FORM_SIZE ,
161137 $ filename ,
162- $ headers [ ' content-type ' ][ 0 ]
138+ $ contentType
163139 );
164140 }
165141
166142 return new UploadedFile (
167- Psr7 \stream_for ($ body ),
168- $ bodyLength ,
143+ Psr7 \stream_for ($ contents ),
144+ $ size ,
169145 UPLOAD_ERR_OK ,
170146 $ filename ,
171- $ headers [ ' content-type ' ][ 0 ]
147+ $ contentType
172148 );
173149 }
174150
175- private function parsePost ($ headers , $ body )
151+ private function parsePost ($ name , $ value )
176152 {
177- foreach ($ headers ['content-disposition ' ] as $ part ) {
178- if (strpos ($ part , 'name ' ) === 0 ) {
179- preg_match ('/name="?(.*)"$/ ' , $ part , $ matches );
180- $ this ->request = $ this ->request ->withParsedBody ($ this ->extractPost (
181- $ this ->request ->getParsedBody (),
182- $ matches [1 ],
183- $ body
184- ));
185-
186- if (strtoupper ($ matches [1 ]) === 'MAX_FILE_SIZE ' ) {
187- $ this ->maxFileSize = (int )$ body ;
188-
189- if ($ this ->maxFileSize === 0 ) {
190- $ this ->maxFileSize = null ;
191- }
192- }
153+ $ this ->request = $ this ->request ->withParsedBody ($ this ->extractPost (
154+ $ this ->request ->getParsedBody (),
155+ $ name ,
156+ $ value
157+ ));
158+
159+ if (strtoupper ($ name ) === 'MAX_FILE_SIZE ' ) {
160+ $ this ->maxFileSize = (int )$ value ;
161+
162+ if ($ this ->maxFileSize === 0 ) {
163+ $ this ->maxFileSize = null ;
193164 }
194165 }
195166 }
@@ -199,47 +170,29 @@ private function parseHeaders($header)
199170 $ headers = array ();
200171
201172 foreach (explode ("\r\n" , trim ($ header )) as $ line ) {
202- list ($ key , $ values ) = explode (': ' , $ line , 2 );
203- $ key = trim ($ key );
204- $ key = strtolower ($ key );
205- $ values = explode ('; ' , $ values );
173+ $ parts = explode (': ' , $ line , 2 );
174+ if (!isset ($ parts [1 ])) {
175+ continue ;
176+ }
177+
178+ $ key = strtolower (trim ($ parts [0 ]));
179+ $ values = explode ('; ' , $ parts [1 ]);
206180 $ values = array_map ('trim ' , $ values );
207181 $ headers [$ key ] = $ values ;
208182 }
209183
210184 return $ headers ;
211185 }
212186
213- private function headerContainsParameter (array $ header , $ parameter )
214- {
215- foreach ($ header as $ part ) {
216- if (strpos ($ part , $ parameter . '= ' ) === 0 ) {
217- return true ;
218- }
219- }
220-
221- return false ;
222- }
223-
224187 private function getParameterFromHeader (array $ header , $ parameter )
225188 {
226189 foreach ($ header as $ part ) {
227- if (strpos ($ part , $ parameter ) === 0 ) {
228- preg_match ('/ ' . $ parameter . '="?(.*)"$/ ' , $ part , $ matches );
190+ if (preg_match ('/ ' . $ parameter . '="?(.*)"$/ ' , $ part , $ matches )) {
229191 return $ matches [1 ];
230192 }
231193 }
232194
233- return '' ;
234- }
235-
236- private function stripTrailingEOL ($ chunk )
237- {
238- if (substr ($ chunk , -2 ) === "\r\n" ) {
239- return substr ($ chunk , 0 , -2 );
240- }
241-
242- return $ chunk ;
195+ return null ;
243196 }
244197
245198 private function extractPost ($ postFields , $ key , $ value )
0 commit comments