Skip to content

Commit 54033d8

Browse files
authored
Merge pull request #274 from clue-labs/upload-ini
Respect ini settings for file uploads
2 parents 64e0b90 + aa77ac3 commit 54033d8

6 files changed

Lines changed: 351 additions & 43 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,8 @@ $handler = function (ServerRequestInterface $request) {
757757
if ($avatar instanceof UploadedFileInterface) {
758758
if ($avatar->getError() === UPLOAD_ERR_OK) {
759759
$uploaded = $avatar->getSize() . ' bytes';
760+
} elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) {
761+
$uploaded = 'file too large';
760762
} else {
761763
$uploaded = 'with error';
762764
}
@@ -782,6 +784,34 @@ $server = new StreamingServer(new MiddlewareRunner([
782784

783785
See also [example #12](examples) for more details.
784786

787+
By default, this middleware respects the
788+
[`upload_max_filesize`](http://php.net/manual/en/ini.core.php#ini.upload-max-filesize)
789+
(default `2M`) ini setting.
790+
Files that exceed this limit will be rejected with an `UPLOAD_ERR_INI_SIZE` error.
791+
You can control the maximum filesize for each individual file upload by
792+
explicitly passing the maximum filesize in bytes as the first parameter to the
793+
constructor like this:
794+
795+
```php
796+
new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file
797+
```
798+
799+
By default, this middleware respects the
800+
[`file_uploads`](http://php.net/manual/en/ini.core.php#ini.file-uploads)
801+
(default `1`) and
802+
[`max_file_uploads`](http://php.net/manual/en/ini.core.php#ini.max-file-uploads)
803+
(default `20`) ini settings.
804+
These settings control if any and how many files can be uploaded in a single request.
805+
If you upload more files in a single request, additional files will be ignored
806+
and the `getUploadedFiles()` method returns a truncated array.
807+
Note that upload fields left blank on submission do not count towards this limit.
808+
You can control the maximum number of file uploads per request by explicitly
809+
passing the second parameter to the constructor like this:
810+
811+
```php
812+
new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each
813+
```
814+
785815
> Note that this middleware handler simply parses everything that is already
786816
buffered in the request body.
787817
It is imperative that the request body is buffered by a prior middleware
@@ -796,13 +826,21 @@ See also [example #12](examples) for more details.
796826
more details.
797827

798828
> PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware.
829+
Files that exceed this limit will be rejected with an `UPLOAD_ERR_FORM_SIZE` error.
799830

800831
> This middleware respects the
801832
[`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars)
802833
(default `1000`) and
803834
[`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level)
804835
(default `64`) ini settings.
805836

837+
> Note that this middleware ignores the
838+
[`enable_post_data_reading`](http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading)
839+
(default `1`) ini setting because it makes little sense to respect here and
840+
is left up to higher-level implementations.
841+
If you want to respect this setting, you have to check its value and
842+
effectively avoid using this middleware entirely.
843+
806844
#### Third-Party Middleware
807845

808846
A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.

examples/12-upload.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
// contents via `(string)$file->getStream()` instead.
4343
// Here, we simply use an inline image to send back to client:
4444
$avatar = '<img src="data:'. $file->getClientMediaType() . ';base64,' . base64_encode($file->getStream()) . '" /> (' . $file->getSize() . ' bytes)';
45+
} elseif ($file->getError() === UPLOAD_ERR_INI_SIZE) {
46+
$avatar = 'upload exceeds file size limit';
4547
} else {
4648
// Real applications should probably check the error number and
4749
// should print some human-friendly text
@@ -119,8 +121,8 @@
119121

120122
// buffer and parse HTTP request body before running our request handler
121123
$server = new StreamingServer(new MiddlewareRunner(array(
122-
new RequestBodyBufferMiddleware(100000), // 100 KB max, ignore body otherwise
123-
new RequestBodyParserMiddleware(),
124+
new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise
125+
new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise
124126
$handler
125127
)));
126128

src/Io/MultipartParser.php

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,32 @@ final class MultipartParser
4747
*/
4848
private $maxInputNestingLevel = 64;
4949

50-
private $postCount = 0;
50+
/**
51+
* ini setting "upload_max_filesize"
52+
*
53+
* @var int
54+
*/
55+
private $uploadMaxFilesize;
5156

52-
public static function parseRequest(ServerRequestInterface $request)
53-
{
54-
$parser = new self($request);
55-
return $parser->parse();
56-
}
57+
/**
58+
* ini setting "max_file_uploads"
59+
*
60+
* Additionally, setting "file_uploads = off" effectively sets this to zero.
61+
*
62+
* @var int
63+
*/
64+
private $maxFileUploads;
5765

58-
private function __construct(ServerRequestInterface $request)
59-
{
60-
$this->request = $request;
66+
private $postCount = 0;
67+
private $filesCount = 0;
68+
private $emptyCount = 0;
6169

70+
/**
71+
* @param int|null $uploadMaxFilesize
72+
* @param int|null $maxFileUploads
73+
*/
74+
public function __construct($uploadMaxFilesize = null, $maxFileUploads = null)
75+
{
6276
$var = ini_get('max_input_vars');
6377
if ($var !== false) {
6478
$this->maxInputVars = (int)$var;
@@ -67,18 +81,29 @@ private function __construct(ServerRequestInterface $request)
6781
if ($var !== false) {
6882
$this->maxInputNestingLevel = (int)$var;
6983
}
84+
85+
$this->uploadMaxFilesize = $uploadMaxFilesize === null ? $this->iniUploadMaxFilesize() : (int)$uploadMaxFilesize;
86+
$this->maxFileUploads = $maxFileUploads === null ? (ini_get('file_uploads') === '' ? 0 : (int)ini_get('max_file_uploads')) : (int)$maxFileUploads;
7087
}
7188

72-
private function parse()
89+
public function parse(ServerRequestInterface $request)
7390
{
74-
$contentType = $this->request->getHeaderLine('content-type');
91+
$contentType = $request->getHeaderLine('content-type');
7592
if(!preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) {
76-
return $this->request;
93+
return $request;
7794
}
7895

79-
$this->parseBody('--' . $matches[1], (string)$this->request->getBody());
96+
$this->request = $request;
97+
$this->parseBody('--' . $matches[1], (string)$request->getBody());
98+
99+
$request = $this->request;
100+
$this->request = null;
101+
$this->postCount = 0;
102+
$this->filesCount = 0;
103+
$this->emptyCount = 0;
104+
$this->maxFileSize = null;
80105

81-
return $this->request;
106+
return $request;
82107
}
83108

84109
private function parseBody($boundary, $buffer)
@@ -137,10 +162,15 @@ private function parsePart($chunk)
137162

138163
private function parseFile($name, $filename, $contentType, $contents)
139164
{
165+
$file = $this->parseUploadedFile($filename, $contentType, $contents);
166+
if ($file === null) {
167+
return;
168+
}
169+
140170
$this->request = $this->request->withUploadedFiles($this->extractPost(
141171
$this->request->getUploadedFiles(),
142172
$name,
143-
$this->parseUploadedFile($filename, $contentType, $contents)
173+
$file
144174
));
145175
}
146176

@@ -150,6 +180,11 @@ private function parseUploadedFile($filename, $contentType, $contents)
150180

151181
// no file selected (zero size and empty filename)
152182
if ($size === 0 && $filename === '') {
183+
// ignore excessive number of empty file uploads
184+
if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) {
185+
return;
186+
}
187+
153188
return new UploadedFile(
154189
Psr7\stream_for(''),
155190
$size,
@@ -159,6 +194,22 @@ private function parseUploadedFile($filename, $contentType, $contents)
159194
);
160195
}
161196

197+
// ignore excessive number of file uploads
198+
if (++$this->filesCount > $this->maxFileUploads) {
199+
return;
200+
}
201+
202+
// file exceeds "upload_max_filesize" ini setting
203+
if ($size > $this->uploadMaxFilesize) {
204+
return new UploadedFile(
205+
Psr7\stream_for(''),
206+
$size,
207+
UPLOAD_ERR_INI_SIZE,
208+
$filename,
209+
$contentType
210+
);
211+
}
212+
162213
// file exceeds MAX_FILE_SIZE value
163214
if ($this->maxFileSize !== null && $size > $this->maxFileSize) {
164215
return new UploadedFile(
@@ -271,4 +322,28 @@ private function extractPost($postFields, $key, $value)
271322

272323
return $postFields;
273324
}
325+
326+
/**
327+
* Gets upload_max_filesize from PHP's configuration expressed in bytes
328+
*
329+
* @return int
330+
* @link http://php.net/manual/en/ini.core.php#ini.upload-max-filesize
331+
* @codeCoverageIgnore
332+
*/
333+
private function iniUploadMaxFilesize()
334+
{
335+
$size = ini_get('upload_max_filesize');
336+
$suffix = strtoupper(substr($size, -1));
337+
if ($suffix === 'K') {
338+
return substr($size, 0, -1) * 1024;
339+
}
340+
if ($suffix === 'M') {
341+
return substr($size, 0, -1) * 1024 * 1024;
342+
}
343+
if ($suffix === 'G') {
344+
return substr($size, 0, -1) * 1024 * 1024 * 1024;
345+
}
346+
347+
return $size;
348+
}
274349
}

src/Middleware/RequestBodyParserMiddleware.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
final class RequestBodyParserMiddleware
99
{
10+
private $multipart;
11+
12+
/**
13+
* @param int|null $uploadMaxFilesize
14+
* @param int|null $maxFileUploads
15+
*/
16+
public function __construct($uploadMaxFilesize = null, $maxFileUploads = null)
17+
{
18+
$this->multipart = new MultipartParser($uploadMaxFilesize, $maxFileUploads);
19+
}
20+
1021
public function __invoke(ServerRequestInterface $request, $next)
1122
{
1223
$type = strtolower($request->getHeaderLine('Content-Type'));
@@ -17,7 +28,7 @@ public function __invoke(ServerRequestInterface $request, $next)
1728
}
1829

1930
if ($type === 'multipart/form-data') {
20-
return $next(MultipartParser::parseRequest($request));
31+
return $next($this->multipart->parse($request));
2132
}
2233

2334
return $next($request);

0 commit comments

Comments
 (0)