RawInput::getBoundary()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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