RawInput   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 106
dl 0
loc 293
rs 9.52
c 0
b 0
f 0
wmc 36

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getParsedFile() 0 25 2
A parse() 0 11 2
A parseFileData() 0 21 2
A detectBlockType() 0 11 3
A arrayParam() 0 9 3
B parseHeaders() 0 28 7
A getParsedParameter() 0 15 3
A getParsedStream() 0 5 1
A getBoundary() 0 11 2
A getBlocks() 0 6 1
B processBlocks() 0 37 7
A addFileToCollection() 0 14 3
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.8
13
 */
14
15
namespace Quantum\Http\Traits\Request;
16
17
use Quantum\Libraries\Storage\Factories\FileSystemFactory;
18
use Quantum\Config\Exceptions\ConfigException;
19
use Quantum\Libraries\Storage\UploadedFile;
20
use Quantum\App\Exceptions\BaseException;
21
use Quantum\Http\Constants\ContentType;
22
use Quantum\Di\Exceptions\DiException;
23
use Quantum\Environment\Server;
24
use ReflectionException;
25
26
/**
27
 * Class RawInput
28
 * @package Quantum\Http
29
 */
30
trait RawInput
31
{
32
33
    /**
34
     * Parses raw input data and returns parsed parameters and files
35
     * @param string $rawInput
36
     * @return array|array[]
37
     * @throws BaseException
38
     * @throws ConfigException
39
     * @throws DiException
40
     * @throws ReflectionException
41
     */
42
    public static function parse(string $rawInput): array
43
    {
44
        $boundary = self::getBoundary();
45
46
        if (!$boundary) {
47
            return ['params' => [], 'files' => []];
48
        }
49
50
        $blocks = self::getBlocks($boundary, $rawInput);
51
52
        return self::processBlocks($blocks);
53
    }
54
55
    /**
56
     * Extracts boundary string from Content-Type header
57
     * @return string|null
58
     */
59
    private static function getBoundary(): ?string
60
    {
61
        $contentType = Server::getInstance()->contentType();
62
63
        if (!$contentType) {
64
            return null;
65
        }
66
67
        preg_match('/boundary=(.*)$/', $contentType, $match);
68
69
        return $match[1] ?? null;
70
    }
71
72
    /**
73
     * Splits raw input into multipart blocks
74
     * @param string $boundary
75
     * @param string $rawInput
76
     * @return array
77
     */
78
    private static function getBlocks(string $boundary, string $rawInput): array
79
    {
80
        $result = preg_split("/-+$boundary/", $rawInput);
81
        array_pop($result);
82
83
        return $result;
84
    }
85
86
    /**
87
     * Processes multipart blocks and extracts parameters and files
88
     * @param array $blocks
89
     * @return array
90
     * @throws BaseException
91
     * @throws ConfigException
92
     * @throws DiException
93
     * @throws ReflectionException
94
     */
95
    private static function processBlocks(array $blocks): array
96
    {
97
        $params = [];
98
        $files = [];
99
100
        foreach ($blocks as $block) {
101
            $block = trim($block);
102
103
            if ($block === '') {
104
                continue;
105
            }
106
107
            $type = self::detectBlockType($block);
108
109
            switch ($type) {
110
                case 'file':
111
                    list($nameParam, $file) = self::getParsedFile($block);
112
113
                    if (!$file) {
114
                        continue 2;
115
                    }
116
117
                    self::addFileToCollection($files, $nameParam, $file);
118
                    break;
119
120
                case 'stream':
121
                    $params += self::getParsedStream($block);
122
                    break;
123
124
                case 'param':
125
                default:
126
                    $params += self::getParsedParameter($block);
127
                    break;
128
            }
129
        }
130
131
        return ['params' => $params, 'files' => $files];
132
    }
133
134
    /**
135
     * Adds a parsed file to the files collection
136
     * @param array $files
137
     * @param string $nameParam
138
     * @param UploadedFile $file
139
     */
140
    private static function addFileToCollection(array &$files, string $nameParam, UploadedFile $file)
141
    {
142
        $arrayParam = self::arrayParam($nameParam);
143
144
        if (is_array($arrayParam)) {
145
            list($name, $key) = $arrayParam;
146
147
            if ($key === '') {
148
                $files[$name][] = $file;
149
            } else {
150
                $files[$name][$key] = $file;
151
            }
152
        } else {
153
            $files[$nameParam] = $file;
154
        }
155
    }
156
157
    /**
158
     * Detects the block type as a string identifier.
159
     * @param string $block
160
     * @return string One of 'file', 'stream', 'param'
161
     */
162
    private static function detectBlockType(string $block): string
163
    {
164
        if (strpos($block, 'filename') !== false) {
165
            return 'file';
166
        }
167
168
        if (strpos($block, ContentType::OCTET_STREAM) !== false) {
169
            return 'stream';
170
        }
171
172
        return 'param';
173
    }
174
175
    /**
176
     * Gets the parsed param
177
     * @param string $block
178
     * @return array
179
     */
180
    private static function getParsedStream(string $block): array
181
    {
182
        preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $block, $match);
183
184
        return [$match[1] => $match[2] ?? ''];
185
    }
186
187
    /**
188
     * Gets the parsed file
189
     * @param string $block
190
     * @return array|null
191
     * @throws BaseException
192
     * @throws ConfigException
193
     * @throws DiException
194
     * @throws ReflectionException
195
     */
196
    private static function getParsedFile(string $block): ?array
197
    {
198
        list($name, $filename, $type, $content) = self::parseFileData($block);
199
200
        if (!$content) {
201
            return null;
202
        }
203
204
        $fs = FileSystemFactory::get();
205
        $tempName = tempnam(sys_get_temp_dir(), 'qt_');
206
        $fs->put($tempName, $content);
207
208
        $file = new UploadedFile([
209
            'name' => $filename,
210
            'type' => $type,
211
            'tmp_name' => $tempName,
212
            'error' => UPLOAD_ERR_OK,
213
            'size' => $fs->size($tempName),
214
        ]);
215
216
        register_shutdown_function(function () use ($fs, $tempName) {
217
            $fs->remove($tempName);
218
        });
219
220
        return [$name, $file];
221
    }
222
223
    /**
224
     * Parses a file block into metadata and binary content
225
     * @param string $block
226
     * @return array{name: string, filename: string, contentType: string, content: string}
227
     */
228
    private static function parseFileData(string $block): array
229
    {
230
        $block = ltrim($block, "\r\n");
231
232
        $parts = explode("\r\n\r\n", $block, 2);
233
234
        if (count($parts) < 2) {
235
            return ['-unknown-', '-unknown-', ContentType::OCTET_STREAM, ''];
236
        }
237
238
        list($rawHeaders, $content) = $parts;
239
240
        list($name, $filename, $contentType) = self::parseHeaders($rawHeaders);
241
242
        $content = substr($content, 0, strlen($content) - 2);
243
244
        return [
245
            $name,
246
            $filename,
247
            $contentType,
248
            $content
249
        ];
250
    }
251
252
    /**
253
     * Parses a block and extracts normal form parameters
254
     * @param string $block
255
     * @return array
256
     */
257
    private static function getParsedParameter(string $block): array
258
    {
259
        $data = [];
260
261
        $block = trim($block);
262
263
        if (preg_match('/name="([^"]+)"\s*\r?\n\r?\n(.*)/s', $block, $match)) {
264
            if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
265
                $data[$tmp[1]][] = rtrim($match[2]);
266
            } else {
267
                $data[$match[1]] = rtrim($match[2]);
268
            }
269
        }
270
271
        return $data;
272
    }
273
274
    /**
275
     * Extracts name, filename, and content type from header lines
276
     * @param string $rawHeaders
277
     * @return array{name: string, filename: string, contentType: string}
278
     */
279
    private static function parseHeaders(string $rawHeaders): array
280
    {
281
        $name = '-unknown-';
282
        $filename = '-unknown-';
283
        $contentType = ContentType::OCTET_STREAM;
284
285
        $rawHeaders = preg_replace("/\r\n|\r|\n/", "\n", $rawHeaders);
286
        $lines = explode("\n", $rawHeaders);
287
288
        foreach ($lines as $line) {
289
            if (stripos($line, 'Content-Disposition') !== false) {
290
                if (preg_match('/name="([^"]+)"/', $line, $match)) {
291
                    $name = $match[1];
292
                }
293
294
                if (preg_match('/filename="([^"]*)"/', $line, $match)) {
295
                    $filename = $match[1];
296
                }
297
            }
298
299
            if (stripos($line, 'Content-Type') !== false) {
300
                if (preg_match('/Content-Type:\s*(.+)/i', $line, $match)) {
301
                    $contentType = trim($match[1]);
302
                }
303
            }
304
        }
305
306
        return [$name, $filename, $contentType];
307
    }
308
309
    /**
310
     * Parses array-like parameter names
311
     * @param string $parameter
312
     * @return array|string
313
     */
314
    private static function arrayParam(string $parameter)
315
    {
316
        if (strpos($parameter, '[') !== false) {
317
            if (preg_match('/^([^[]*)\[([^]]*)\](.*)$/', $parameter, $match)) {
318
                return [$match[1], $match[2]];
319
            }
320
        }
321
322
        return $parameter;
323
    }
324
}