Completed
Push — master ( 61f375...73f1c9 )
by Andrew
01:52
created

Request   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 3
dl 0
loc 305
rs 5.5199
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A getParams() 0 4 1
A cleanUploadedFiles() 0 6 2
A setBufferSize() 0 4 1
A getBufferSize() 0 4 1
A setUploadDir() 0 4 1
A getUploadDir() 0 4 2
A getQuery() 0 10 3
B getPost() 0 27 9
C parseMultipartFormData() 0 54 16
A getCookies() 0 15 3
A getStdin() 0 4 1
A getServerRequest() 0 25 2
A getHttpFoundationRequest() 0 12 2
B addFile() 0 31 7
A preparePsr7UploadedFiles() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like Request often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Request, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PHPFastCGI\FastCGIDaemon\Http;
4
5
use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;
6
use function Zend\Diactoros\createUploadedFile;
7
use Zend\Diactoros\ServerRequest;
8
use Zend\Diactoros\ServerRequestFactory;
9
10
/**
11
 * The default implementation of the RequestInterface.
12
 */
13
final class Request implements RequestInterface
14
{
15
    /**
16
     * @var int
17
     */
18
    private static $bufferSize = 10485760; // 10 MB
19
20
    /**
21
     * @var string
22
     */
23
    private static $uploadDir = null;
24
25
    /**
26
     * @var array
27
     */
28
    private $uploadedFiles = [];
29
30
    /**
31
     * @var array
32
     */
33
    private $params;
34
35
    /**
36
     * @var resource
37
     */
38
    private $stdin;
39
40
    /**
41
     * Constructor.
42
     *
43
     * @param array    $params The FastCGI server params as an associative array
44
     * @param resource $stdin  The FastCGI stdin data as a stream resource
45
     */
46
    public function __construct(array $params, $stdin)
47
    {
48
        $this->params = [];
49
50
        foreach ($params as $name => $value) {
51
            $this->params[strtoupper($name)] = $value;
52
        }
53
54
        $this->stdin  = $stdin;
55
56
        rewind($this->stdin);
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function getParams()
63
    {
64
        return $this->params;
65
    }
66
67
    /**
68
     * Remove all uploaded files
69
     */
70
    public function cleanUploadedFiles(): void
71
    {
72
        foreach ($this->uploadedFiles as $file) {
73
            @unlink($file['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
74
        }
75
    }
76
77
    /**
78
     * Set a buffer size to read uploaded files
79
     */
80
    public static function setBufferSize(int $size): void
81
    {
82
        static::$bufferSize = $size;
0 ignored issues
show
Comprehensibility introduced by
Since PHPFastCGI\FastCGIDaemon\Http\Request is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
83
    }
84
85
    public static function getBufferSize(): int
86
    {
87
        return static::$bufferSize;
0 ignored issues
show
Comprehensibility introduced by
Since PHPFastCGI\FastCGIDaemon\Http\Request is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
88
    }
89
90
    public static function setUploadDir(string $dir): void
91
    {
92
        static::$uploadDir = $dir;
0 ignored issues
show
Comprehensibility introduced by
Since PHPFastCGI\FastCGIDaemon\Http\Request is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
93
    }
94
95
    public static function getUploadDir(): string
96
    {
97
        return static::$uploadDir ?: sys_get_temp_dir();
0 ignored issues
show
Comprehensibility introduced by
Since PHPFastCGI\FastCGIDaemon\Http\Request is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function getQuery()
104
    {
105
        $query = null;
106
107
        if (isset($this->params['QUERY_STRING'])) {
108
            parse_str($this->params['QUERY_STRING'], $query);
109
        }
110
111
        return $query ?: [];
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function getPost()
118
    {
119
        $post = null;
120
121
        if (isset($this->params['REQUEST_METHOD']) && isset($this->params['CONTENT_TYPE'])) {
122
            $requestMethod = $this->params['REQUEST_METHOD'];
123
            $contentType   = $this->params['CONTENT_TYPE'];
124
125
            if (strcasecmp($requestMethod, 'POST') === 0 && stripos($contentType, 'multipart/form-data') === 0) {
126
                if (preg_match('/boundary=(?P<quote>[\'"]?)(.*)(?P=quote)/', $contentType, $matches)) {
127
                    list($postData, $this->uploadedFiles) = $this->parseMultipartFormData($this->stdin, $matches[2]);
128
                    parse_str($postData, $post);
129
130
                    return $post;
131
                }
132
            }
133
134
            if (strcasecmp($requestMethod, 'POST') === 0 && stripos($contentType, 'application/x-www-form-urlencoded') === 0) {
135
                $postData = stream_get_contents($this->stdin);
136
                rewind($this->stdin);
137
138
                parse_str($postData, $post);
139
            }
140
        }
141
142
        return $post ?: [];
143
    }
144
145
    private function parseMultipartFormData($stream, $boundary) {
146
        $post = "";
147
        $files = [];
148
        $fieldType = $fieldName = $filename = $mimeType = null;
149
        $inHeader = $getContent = false;
0 ignored issues
show
Unused Code introduced by
$getContent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
150
151
        while (!feof($stream)) {
152
            $getContent = $fieldName && !$inHeader;
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
153
            $buffer = stream_get_line($stream, static::$bufferSize,  "\n" . ($getContent ? '--'.$boundary : ''));
0 ignored issues
show
Comprehensibility introduced by
Since PHPFastCGI\FastCGIDaemon\Http\Request is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
154
            $buffer = trim($buffer, "\r");
155
156
            // Find the empty line between headers and body
157
            if ($inHeader && strlen($buffer) == 0) {
158
                $inHeader = false;
159
160
                continue;
161
            }
162
163
            if ($getContent) {
164
                if ($fieldType === 'data') {
165
                    $post .= (isset($post[0]) ? '&' : '') . $fieldName . "=" . urlencode($buffer);
166
                } elseif ($fieldType === 'file' && $filename) {
167
                    $tmpPath = tempnam($this->getUploadDir(), 'fastcgi_upload');
168
                    $err = file_put_contents($tmpPath, $buffer);
169
                    $this->addFile($files, $fieldName, $filename, $tmpPath, $mimeType, false === $err);
170
                    $filename = $mimeType = null;
171
                }
172
                $fieldName = $fieldType = null;
173
174
                continue;
175
            }
176
177
            // Assert: We may be in the header, lets try to find 'Content-Disposition' and 'Content-Type'.
178
            if (strpos($buffer, 'Content-Disposition') === 0) {
179
                $inHeader = true;
180
                if (preg_match('/name=\"([^\"]*)\"/', $buffer, $matches)) {
181
                    $fieldName = $matches[1];
182
                }
183
                if (preg_match('/filename=\"([^\"]*)\"/', $buffer, $matches)) {
184
                    $filename = $matches[1];
185
                    $fieldType = 'file';
186
                } else {
187
                    $fieldType = 'data';
188
                }
189
            } elseif (strpos($buffer, 'Content-Type') === 0) {
190
                $inHeader = true;
191
                if (preg_match('/Content-Type: (.*)?/', $buffer, $matches)) {
192
                    $mimeType = trim($matches[1]);
193
                }
194
            }
195
        }
196
197
        return [$post, $files];
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function getCookies()
204
    {
205
        $cookies = [];
206
207
        if (isset($this->params['HTTP_COOKIE'])) {
208
            $cookiePairs = explode(';', $this->params['HTTP_COOKIE']);
209
210
            foreach ($cookiePairs as $cookiePair) {
211
                list($name, $value) = explode('=', trim($cookiePair));
212
                $cookies[$name] = $value;
213
            }
214
        }
215
216
        return $cookies;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222
    public function getStdin()
223
    {
224
        return $this->stdin;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function getServerRequest()
231
    {
232
        if (!class_exists(ServerRequest::class)) {
233
            throw new \RuntimeException('You need to install zendframework/zend-diactoros^1.8 to use PSR-7 requests.');
234
        }
235
236
        $query   = $this->getQuery();
237
        $post    = $this->getPost();
238
        $cookies = $this->getCookies();
239
240
        $server  = ServerRequestFactory::normalizeServer($this->params);
241
        $headers = ServerRequestFactory::marshalHeaders($server);
0 ignored issues
show
Deprecated Code introduced by
The method Zend\Diactoros\ServerReq...ctory::marshalHeaders() has been deprecated with message: since 1.8.0; use Zend\Diactoros\marshalHeadersFromSapi().

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
242
        $uri     = ServerRequestFactory::marshalUriFromServer($server, $headers);
0 ignored issues
show
Deprecated Code introduced by
The method Zend\Diactoros\ServerReq...:marshalUriFromServer() has been deprecated with message: since 1.8.0; use Zend\Diactoros\marshalUriFromSapi() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
243
        $method  = ServerRequestFactory::get('REQUEST_METHOD', $server, 'GET');
0 ignored issues
show
Deprecated Code introduced by
The method Zend\Diactoros\ServerRequestFactory::get() has been deprecated with message: since 1.8.0; no longer used internally.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
244
245
        $files = $this->uploadedFiles;
246
        $this->preparePsr7UploadedFiles($files);
247
248
        $request = new ServerRequest($server, $files, $uri, $method, $this->stdin, $headers);
249
250
        return $request
251
            ->withCookieParams($cookies)
252
            ->withQueryParams($query)
253
            ->withParsedBody($post);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getHttpFoundationRequest()
260
    {
261
        if (!class_exists(HttpFoundationRequest::class)) {
262
            throw new \RuntimeException('You need to install symfony/http-foundation:^4.0 to use HttpFoundation requests.');
263
        }
264
265
        $query   = $this->getQuery();
266
        $post    = $this->getPost();
267
        $cookies = $this->getCookies();
268
269
        return new HttpFoundationRequest($query, $post, [], $cookies, $this->uploadedFiles, $this->params, $this->stdin);
0 ignored issues
show
Bug introduced by
It seems like $post defined by $this->getPost() on line 266 can also be of type null; however, Symfony\Component\HttpFo...\Request::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
270
    }
271
272
    /**
273
     * Add a file to the $files array
274
     */
275
    private function addFile(array &$files, string $fieldName, string $filename, string $tmpPath, string $mimeType, bool $err): void
276
    {
277
        $data = [
278
            'type' => $mimeType ?: 'application/octet-stream',
279
            'name' => $filename,
280
            'tmp_name' => $tmpPath,
281
            'error' => $err ? UPLOAD_ERR_CANT_WRITE : UPLOAD_ERR_OK,
282
            'size' => filesize($tmpPath),
283
        ];
284
285
        $parts = preg_split('|(\[[^\]]*\])|', $fieldName, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
286
        $count = count($parts);
287
        if (1 === $count) {
288
            $files[$fieldName] = $data;
289
        } else {
290
            $current = &$files;
291
            foreach ($parts as $i => $part) {
292
                if ($part === '[]') {
293
                    $current[] = $data;
294
                    continue;
295
                }
296
297
                $trimmedMatch = trim($part, '[]');
298
                if ($i === $count -1) {
299
                    $current[$trimmedMatch] = $data;
300
                } else {
301
                    $current = &$current[$trimmedMatch];
302
                }
303
            }
304
        }
305
    }
306
307
    private function preparePsr7UploadedFiles(array &$files)
308
    {
309
        if (isset($files['tmp_name'])) {
310
            $files = createUploadedFile($files);
311
        } else {
312
            foreach ($files as &$file) {
313
                $this->preparePsr7UploadedFiles($file);
0 ignored issues
show
Documentation introduced by
$file is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
314
            }
315
        }
316
    }
317
}
318