Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.50% covered (success)
97.50%
117 / 120
90.48% covered (success)
90.48%
19 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
97.50% covered (success)
97.50%
117 / 120
90.48% covered (success)
90.48%
19 / 21
78
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 hasFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPost
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getFiles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPut
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPatch
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDelete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getQueryData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getParsedData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getRawData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasQueryData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParsedData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRawData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStreamToFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStreamToFileLocation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 processStreamToFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 processData
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
29
 prepareStreamToFile
88.24% covered (success)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
10.16
 clearStreamToFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 __get
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Server;
15
16use Pop\Http\Parser;
17use Pop\Http\HttpFilterableTrait;
18
19/**
20 * HTTP server request data class
21 *
22 * @category   Pop
23 * @package    Pop\Http
24 * @author     Nick Sagona, III <dev@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    5.2.0
28 */
29class Data
30{
31
32    use HttpFilterableTrait;
33
34    /**
35     * GET array
36     * @var array
37     */
38    protected array $get = [];
39
40    /**
41     * POST array
42     * @var array
43     */
44    protected array $post = [];
45
46    /**
47     * FILES array
48     * @var array
49     */
50    protected array $files = [];
51
52    /**
53     * PUT array
54     * @var array
55     */
56    protected array $put = [];
57
58    /**
59     * PATCH array
60     * @var array
61     */
62    protected array $patch = [];
63
64    /**
65     * DELETE array
66     * @var array
67     */
68    protected array $delete = [];
69
70    /**
71     * Query data
72     * @var mixed
73     */
74    protected mixed $queryData = null;
75
76    /**
77     * Parsed data
78     * @var mixed
79     */
80    protected mixed $parsedData = null;
81
82    /**
83     * Raw data
84     * @var mixed
85     */
86    protected mixed $rawData = null;
87
88    /**
89     * Stream to file
90     * @var bool
91     */
92    protected bool $streamToFile = false;
93
94    /**
95     * Stream to file
96     * @var ?string
97     */
98    protected ?string $streamToFileLocation = null;
99
100    /**
101     * Constructor
102     *
103     * Instantiate the request data object
104     *
105     * @param  ?string $contentType
106     * @param  ?string $encoding
107     * @param  mixed $filters
108     * @param  mixed $streamToFile
109     * @throws Exception
110     */
111    public function __construct(?string $contentType = null, ?string $encoding = null, mixed $filters = null, mixed $streamToFile = null)
112    {
113        if ($filters !== null) {
114            if (is_array($filters)) {
115                $this->addFilters($filters);
116            } else {
117                $this->addFilter($filters);
118            }
119        }
120
121        $this->get   = (isset($_GET))   ? $_GET   : [];
122        $this->post  = (isset($_POST))  ? $_POST  : [];
123        $this->files = (isset($_FILES)) ? $_FILES : [];
124
125        if (isset($_SERVER['REQUEST_METHOD'])) {
126            $this->processData($contentType, $encoding, $streamToFile);
127        }
128    }
129
130    /**
131     * Return whether or not the request has FILES
132     *
133     * @return bool
134     */
135    public function hasFiles(): bool
136    {
137        return (count($this->files) > 0);
138    }
139
140    /**
141     * Get a value from $_GET, or the whole array
142     *
143     * @param  ?string $key
144     * @return string|array|null
145     */
146    public function getQuery(?string $key = null): string|array|null
147    {
148        if ($key === null) {
149            return $this->get;
150        } else {
151            return $this->get[$key] ?? null;
152        }
153    }
154
155    /**
156     * Get a value from $_POST, or the whole array
157     *
158     * @param  ?string $key
159     * @return string|array|null
160     */
161    public function getPost(?string $key = null): string|array|null
162    {
163        if ($key === null) {
164            return $this->post;
165        } else {
166            return $this->post[$key] ?? null;
167        }
168    }
169
170    /**
171     * Get a value from $_FILES, or the whole array
172     *
173     * @param  ?string $key
174     * @return string|array|null
175     */
176    public function getFiles(?string $key = null): string|array|null
177    {
178        if ($key === null) {
179            return $this->files;
180        } else {
181            return $this->files[$key] ?? null;
182        }
183    }
184
185    /**
186     * Get a value from PUT query data, or the whole array
187     *
188     * @param  ?string $key
189     * @return string|array|null
190     */
191    public function getPut(?string $key = null): string|array|null
192    {
193        if ($key === null) {
194            return $this->put;
195        } else {
196            return $this->put[$key] ?? null;
197        }
198    }
199
200    /**
201     * Get a value from PATCH query data, or the whole array
202     *
203     * @param  ?string $key
204     * @return string|array|null
205     */
206    public function getPatch(?string $key = null): string|array|null
207    {
208        if ($key === null) {
209            return $this->patch;
210        } else {
211            return $this->patch[$key] ?? null;
212        }
213    }
214
215    /**
216     * Get a value from DELETE query data, or the whole array
217     *
218     * @param  ?string $key
219     * @return string|array|null
220     */
221    public function getDelete(?string $key = null): string|array|null
222    {
223        if ($key === null) {
224            return $this->delete;
225        } else {
226            return $this->delete[$key] ?? null;
227        }
228    }
229
230    /**
231     * Get a value from query data, or the whole array
232     *
233     * @param  ?string $key
234     * @return string|array|null
235     */
236    public function getQueryData(?string $key = null): string|array|null
237    {
238        $result = null;
239
240        if (is_array($this->queryData)) {
241            if ($key === null) {
242                $result = $this->queryData;
243            } else {
244                $result = $this->queryData[$key] ?? null;
245            }
246        }
247
248        return $result;
249    }
250
251    /**
252     * Get a value from parsed data, or the whole array
253     *
254     * @param  ?string $key
255     * @return string|array|null
256     */
257    public function getParsedData(?string $key = null): string|array|null
258    {
259        $result = null;
260
261        if (is_array($this->parsedData)) {
262            if ($key === null) {
263                $result = $this->parsedData;
264            } else {
265                $result = $this->parsedData[$key] ?? null;
266            }
267        }
268
269        return $result;
270    }
271
272    /**
273     * Get the raw data
274     *
275     * @return string|null
276     */
277    public function getRawData(): string|null
278    {
279        return $this->rawData;
280    }
281
282    /**
283     * Has query data
284     *
285     * @return bool
286     */
287    public function hasQueryData(): bool
288    {
289        return !empty($this->queryData);
290    }
291
292    /**
293     * Has parsed data
294     *
295     * @return bool
296     */
297    public function hasParsedData(): bool
298    {
299        return !empty($this->parsedData);
300    }
301
302    /**
303     * Has raw data
304     *
305     * @return bool
306     */
307    public function hasRawData(): bool
308    {
309        return !empty($this->rawData);
310    }
311
312    /**
313     * Is the request stream to file
314     *
315     * @return bool
316     */
317    public function isStreamToFile(): bool
318    {
319        return $this->streamToFile;
320    }
321
322    /**
323     * Get stream to file location
324     *
325     * @return string
326     */
327    public function getStreamToFileLocation(): string
328    {
329        return $this->streamToFileLocation;
330    }
331
332    /**
333     * Process stream to file
334     *
335     * @param  ?string $contentType
336     * @param  ?string $contentEncoding
337     * @return Data
338     */
339    public function processStreamToFile(?string $contentType = null, ?string $contentEncoding = null): Data
340    {
341        if (($this->streamToFile) && file_exists($this->streamToFileLocation)) {
342            $this->rawData    = file_get_contents($this->streamToFileLocation);
343            $this->parsedData = Parser::parseDataByContentType($this->rawData, $contentType, $contentEncoding);
344        }
345
346        return $this;
347    }
348
349    /**
350     * Process any data that came with the request
351     *
352     * @param  ?string $contentType
353     * @param  ?string $encoding
354     * @param  mixed  $streamToFile
355     * @throws Exception
356     * @return void
357     */
358    public function processData(?string $contentType = null, ?string $encoding = null, mixed $streamToFile = null)
359    {
360        // Stream raw data to file location
361        if ($streamToFile !== null) {
362            $this->prepareStreamToFile($streamToFile);
363        } else {
364            /**
365             * $_SERVER['X_POP_HTTP_RAW_DATA'] is for testing purposes only
366             */
367            $this->rawData = (isset($_SERVER['X_POP_HTTP_RAW_DATA'])) ?
368                $_SERVER['X_POP_HTTP_RAW_DATA'] : file_get_contents('php://input');
369        }
370
371        // Process query string
372        if (isset($_SERVER['QUERY_STRING'])) {
373            $this->queryData = rawurldecode($_SERVER['QUERY_STRING']);
374            $this->queryData = (($contentType !== null) && ((stripos($contentType, 'json') !== false) || (stripos($contentType, 'xml') !== false))) ?
375                Parser::parseDataByContentType($this->queryData, $contentType, $encoding) :
376                Parser::parseDataByContentType($this->queryData, 'application/x-www-form-urlencoded', $encoding);
377
378            if (empty($this->rawData)) {
379                $this->rawData = $_SERVER['QUERY_STRING'];
380            }
381        }
382
383        // Process raw data
384        if (($contentType !== null) && ($this->rawData !== null)) {
385            $this->parsedData = Parser::parseDataByContentType($this->rawData, $contentType, $encoding);
386        }
387
388        // If the query string had a processed custom data string
389        if ((strtoupper($_SERVER['REQUEST_METHOD']) == 'GET') && ($this->get != $this->queryData) && !empty($this->queryData)) {
390            $this->get = $this->queryData;
391            // If the request was POST and had processed custom data
392        } else if ((strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') && ($this->post != $this->parsedData) && !empty($this->parsedData)) {
393            $this->post = $this->parsedData;
394        }
395
396        if (empty($this->parsedData)) {
397            if (!empty($this->get)) {
398                $this->parsedData = $this->get;
399            } else if (!empty($this->post)) {
400                $this->parsedData = $this->post;
401            }
402        }
403
404        // If request data has filters, filter parsed input data
405        if ($this->hasFilters()) {
406            if (!empty($this->parsedData)) {
407                $this->parsedData = $this->filter($this->parsedData);
408            }
409            if (!empty($this->post)) {
410                $this->post = $this->filter($this->post);
411            }
412            if (!empty($this->get)) {
413                $this->get = $this->filter($this->get);
414            }
415        }
416
417        // Set parsed data to the proper method-based array
418        switch (strtoupper($_SERVER['REQUEST_METHOD'])) {
419            case 'PUT':
420                $this->put = (!empty($this->parsedData)) ? $this->parsedData : [];
421                break;
422
423            case 'PATCH':
424                $this->patch = (!empty($this->parsedData)) ? $this->parsedData : [];
425                break;
426
427            case 'DELETE':
428                $this->delete = (!empty($this->parsedData)) ? $this->parsedData : [];
429                break;
430        }
431    }
432
433    /**
434     * Prepare stream to file
435     *
436     * @param  mixed $streamToFile
437     * @throws Exception
438     * @return void
439     */
440    public function prepareStreamToFile(mixed $streamToFile): void
441    {
442        $this->streamToFile = true;
443
444        // Stream raw data to system temp folder with auto-generated filename
445        if ($streamToFile === true) {
446            $this->streamToFileLocation = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pop-http-' . uniqid();
447            // Else, stream raw data to user-specified file location
448        } else if (!is_dir($streamToFile) && is_dir(dirname($streamToFile)) && is_writable(dirname($streamToFile))) {
449            $this->streamToFileLocation = $streamToFile;
450            // Else, stream raw data to user-specified direction with auto-generated filename
451        } else if (is_dir($streamToFile) && is_writable($streamToFile)) {
452            $filename = 'pop-http-' . uniqid();
453            $this->streamToFileLocation = $streamToFile .
454                ((substr($streamToFile, -1) == DIRECTORY_SEPARATOR) ? $filename : DIRECTORY_SEPARATOR . $filename);
455        } else {
456            throw new Exception('Error: Unable to determine an acceptable file location in which to stream the data.');
457        }
458
459        /**
460         * $_SERVER['X_POP_HTTP_RAW_DATA'] is for testing purposes only
461         */
462        if (!empty($this->streamToFileLocation)) {
463            $this->rawData = ($_SERVER['X_POP_HTTP_RAW_DATA'] ?? file_get_contents('php://input'));
464            file_put_contents($this->streamToFileLocation, $this->rawData);
465
466            clearstatcache();
467
468            // Clear out if no raw data was stored
469            if (filesize($this->streamToFileLocation) == 0) {
470                unlink($this->streamToFileLocation);
471                $this->streamToFileLocation = null;
472            }
473        }
474    }
475
476    /**
477     * Clear stream to file
478     *
479     * @return Data
480     */
481    public function clearStreamToFile(): Data
482    {
483        if (file_exists($this->streamToFileLocation) && is_writable($this->streamToFileLocation)) {
484            unlink($this->streamToFileLocation);
485        }
486        return $this;
487    }
488
489    /**
490     * Magic method to get a value from one of the data arrays
491     *
492     * @param  string $name
493     * @return mixed
494     */
495    public function __get(string $name): mixed
496    {
497        return match ($name) {
498            'get'    => $this->get,
499            'post'   => $this->post,
500            'files'  => $this->files,
501            'put'    => $this->put,
502            'patch'  => $this->patch,
503            'delete' => $this->delete,
504            'parsed' => $this->parsedData,
505            'raw'    => $this->rawData,
506            default  => null,
507        };
508    }
509
510}