Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.78% covered (success)
90.78%
128 / 141
86.21% covered (success)
86.21%
25 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
90.78% covered (success)
90.78%
128 / 141
86.21% covered (success)
86.21%
25 / 29
105.53
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 setData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 addData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 removeAllData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setRequest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataContentLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasDataContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPrepared
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
16.10
 reset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prepareUrlEncoded
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 prepareJson
77.27% covered (success)
77.27%
17 / 22
0.00% covered (danger)
0.00%
0 / 1
20.39
 prepareXml
72.22% covered (success)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
16.62
 prepareMultipart
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getRawData
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
2
 getRawDataLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMimeTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMimeTypeFromFilename
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getDefaultMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Client;
15
16use Pop\Http\HttpFilterableTrait;
17use Pop\Mime\Message;
18use Pop\Mime\Part;
19
20/**
21 * Client request data class
22 *
23 * @category   Pop
24 * @package    Pop\Http
25 * @author     Nick Sagona, III <dev@noladev.com>
26 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    5.3.2
29 */
30class Data
31{
32
33    use HttpFilterableTrait;
34
35    /**
36     * Raw data constant
37     */
38    const POP_CLIENT_REQUEST_RAW_DATA = 'POP_CLIENT_REQUEST_RAW_DATA';
39
40    /**
41     * Data fields (form fields and files)
42     *    $data = [
43     *      'username' => 'admin'
44     *      'file1'     => [
45     *          'filename'    => __DIR__ . '/path/to/file.txt',
46     *          'contentType' => 'text/plain'
47     *      ],
48     *      'file2'     => [
49     *          'filename'    => 'test.pdf',
50     *          'contentType' => 'application/pdf',
51     *          'contents'    => file_get_contents(__DIR__ . '/path/to/test.pdf'
52     *      ]
53     *    ]
54     * @var array
55     */
56    protected array $data = [];
57
58    /**
59     * Data parent request
60     * @var ?Request
61     */
62    protected ?Request $request = null;
63
64    /**
65     * Data content
66     * @var ?string
67     */
68    protected ?string $dataContent = null;
69
70    /**
71     * Data content prepared flag
72     * @var bool
73     */
74    protected bool $prepared = false;
75
76    /**
77     * Common mime types
78     * @var array
79     */
80    protected static array $mimeTypes = [
81        'bmp'    => 'image/x-ms-bmp',
82        'bz2'    => 'application/bzip2',
83        'csv'    => 'text/csv',
84        'doc'    => 'application/msword',
85        'docx'   => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
86        'gif'    => 'image/gif',
87        'gz'     => 'application/gzip',
88        'jpe'    => 'image/jpeg',
89        'jpg'    => 'image/jpeg',
90        'jpeg'   => 'image/jpeg',
91        'json'   => 'application/json',
92        'log'    => 'text/plain',
93        'pdf'    => 'application/pdf',
94        'png'    => 'image/png',
95        'ppt'    => 'application/msword',
96        'pptx'   => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
97        'psd'    => 'image/x-photoshop',
98        'svg'    => 'image/svg+xml',
99        'tar'    => 'application/x-tar',
100        'tbz'    => 'application/bzip2',
101        'tbz2'   => 'application/bzip2',
102        'tgz'    => 'application/gzip',
103        'tif'    => 'image/tiff',
104        'tiff'   => 'image/tiff',
105        'tsv'    => 'text/tsv',
106        'txt'    => 'text/plain',
107        'xls'    => 'application/msword',
108        'xlsx'   => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
109        'xml'    => 'application/xml',
110        'zip'    => 'application/zip'
111    ];
112
113    /**
114     * Default mime type
115     * @var string
116     */
117    protected static string $defaultMimeType = 'application/octet-stream';
118
119    /**
120     * Constructor
121     *
122     * Instantiate the request data object
123     *
124     * @param array|string $data
125     * @param mixed        $filters
126     * @param ?string      $type
127     */
128    public function __construct(array|string $data = [], mixed $filters = null, ?Request $request = null)
129    {
130        if ($filters !== null) {
131            if (is_array($filters)) {
132                $this->addFilters($filters);
133            } else {
134                $this->addFilter($filters);
135            }
136        }
137
138        if ($request !== null) {
139            $this->setRequest($request);
140        }
141
142        if (!empty($data)) {
143            $this->setData($data);
144        }
145    }
146
147    /**
148     * Set data
149     *
150     * @param  array|string $data
151     * @return Data
152     */
153    public function setData(array|string $data): Data
154    {
155        if (is_string($data)) {
156            $this->data = [self::POP_CLIENT_REQUEST_RAW_DATA => $data];
157        } else if (is_array($data) && (count($data) == 1) && isset($data[0])) {
158            $this->data = [self::POP_CLIENT_REQUEST_RAW_DATA => $data[0]];
159        } else {
160            $this->data = $data;
161            $this->prepare();
162        }
163
164        return $this;
165    }
166
167    /**
168     * Add data
169     *
170     * @param  array|string $data
171     * @param  mixed        $value
172     * @return Data
173     */
174    public function addData(array|string $data, mixed $value = null): Data
175    {
176        if (is_string($data) && ($value !== null)) {
177            $this->data[$data] = $value;
178        } else if (is_array($data)) {
179            $this->data = array_merge($this->data, $data);
180        }
181
182        $this->prepare();
183
184        return $this;
185    }
186
187    /**
188     * Get data
189     *
190     * @param  ?string $key
191     * @return mixed
192     */
193    public function getData(?string $key = null): mixed
194    {
195        if ($key !== null) {
196            return $this->data[$key] ?? null;
197        } else {
198            return $this->data;
199        }
200    }
201
202    /**
203     * Has data
204     *
205     * @param  ?string $key
206     * @return bool
207     */
208    public function hasData(?string $key = null): bool
209    {
210        if ($key !== null) {
211            return (isset($this->data[$key]));
212        } else {
213            return !empty($this->data);
214        }
215    }
216
217    /**
218     * Remove data
219     *
220     * @param  string $key
221     * @return Data
222     */
223    public function removeData(string $key): Data
224    {
225        if (isset($this->data[$key])) {
226            unset($this->data[$key]);
227        }
228
229        $this->prepare();
230
231        return $this;
232    }
233
234    /**
235     * Remove all data
236     *
237     * @return Data
238     */
239    public function removeAllData(): Data
240    {
241        $this->data        = [];
242        $this->dataContent = null;
243
244        return $this;
245    }
246
247    /**
248     * Set data parent request
249     *
250     * @param  Request $request
251     * @return Data
252     */
253    public function setRequest(Request $request): Data
254    {
255        $this->request = $request;
256        return $this;
257    }
258
259    /**
260     * Get data parent request
261     *
262     * @return ?Request
263     */
264    public function getRequest(): ?Request
265    {
266        return $this->request;
267    }
268
269    /**
270     * Has data parent request
271     *
272     * @return bool
273     */
274    public function hasRequest(): bool
275    {
276        return !empty($this->request);
277    }
278
279    /**
280     * Get prepared data content
281     *
282     * @return ?string
283     */
284    public function getDataContent(): ?string
285    {
286        return $this->dataContent;
287    }
288
289    /**
290     * Get data content length
291     *
292     * @param  bool $mb
293     * @return int
294     */
295    public function getDataContentLength(bool $mb = false): int
296    {
297        return ($mb) ? mb_strlen($this->dataContent) : strlen($this->dataContent);
298    }
299
300    /**
301     * Check if the data content has been prepared
302     *
303     * @return bool
304     */
305    public function hasDataContent(): bool
306    {
307        return !empty($this->dataContent);
308    }
309
310    /**
311     * Check if the data content has been prepared (alias to hasDataContent)
312     *
313     * @return bool
314     */
315    public function isPrepared(): bool
316    {
317        return $this->prepared;
318    }
319
320    /**
321     * Prepare data
322     *
323     * @param  bool $mb
324     * @return Data
325     */
326    public function prepare(bool $mb = false): Data
327    {
328        $type = $this->request?->getRequestType();
329        switch ($type) {
330            case Request::JSON:
331                $this->prepareJson();
332                break;
333            case Request::XML:
334                $this->prepareXml();
335                break;
336            case Request::URLENCODED:
337                $this->prepareUrlEncoded();
338                break;
339            case Request::MULTIPART:
340                $this->prepareMultipart();
341                break;
342            default:
343                // Custom types
344                if (($type !== null) && (strrpos($type, 'json') !== false)) {
345                    $this->prepareJson();
346                } else if (($type !== null) && (strrpos($type, 'xml') !== false)) {
347                    $this->prepareXml();
348                } else {
349                    if ($this->hasRawData()) {
350                        $this->dataContent = $this->getRawData();
351                    } else if ($this->hasData()) {
352                        $this->prepareUrlEncoded();
353                    }
354                }
355
356        }
357
358        if (!empty($this->dataContent) && ($this->hasRequest()) && ($this->request->getMethod() != 'GET')) {
359            if ($this->request->hasHeader('Content-Length')) {
360                $this->request->removeHeader('Content-Length');
361            }
362            $this->request->addHeader('Content-Length', strlen($this->dataContent));
363        }
364
365        $this->prepared = true;
366
367        return $this;
368    }
369
370    /**
371     * Reset data
372     *
373     * @return Data
374     */
375    public function reset(): Data
376    {
377        $this->dataContent = null;
378        $this->prepared    = false;
379        return $this;
380    }
381
382    /**
383     * Method to prepare URL-encoded data content
384     *
385     * @return Data
386     */
387    public function prepareUrlEncoded(): Data
388    {
389        if (!array_key_exists(self::POP_CLIENT_REQUEST_RAW_DATA, $this->data) && !empty($this->data)) {
390            $data = $this->data;
391            if ($this->hasFilters()) {
392                $data = $this->filter($data);
393            }
394            $this->dataContent = http_build_query($data);
395        } else {
396            $this->dataContent = null;
397        }
398
399        if ($this->hasRequest()) {
400            if ($this->request->hasHeader('Content-Type')) {
401                $this->request->removeHeader('Content-Type');
402            }
403            $this->request->addHeader('Content-Type', Request::URLENCODED);
404        }
405
406        return $this;
407    }
408
409    /**
410     * Method to prepare JSON data content
411     *
412     * @return Data
413     */
414    public function prepareJson(): Data
415    {
416        if ($this->hasRawData()) {
417            $jsonContent = $this->getRawData();
418        } else {
419            $jsonData    = $this->data;
420            $jsonContent = [];
421
422            // Check for JSON files
423            foreach ($jsonData as $jsonDatum) {
424                if (isset($jsonDatum['filename']) && isset($jsonDatum['contentType']) &&
425                    str_contains(strtolower($jsonDatum['contentType']), 'json') && file_exists($jsonDatum['filename'])) {
426                    $jsonContent = array_merge($jsonContent, json_decode(file_get_contents($jsonDatum['filename']), true));
427                }
428            }
429
430            // Else, use JSON data
431            if (empty($jsonContent)) {
432                $jsonContent = $jsonData;
433            }
434        }
435
436        if ($this->hasRequest()) {
437            if ($this->request->hasHeader('Content-Type') && !str_contains(strtolower((string)$this->request->getHeader('Content-Type')), 'json')) {
438                $this->request->removeHeader('Content-Type');
439            }
440            if (!$this->request->hasHeader('Content-Type')) {
441                $type = $this->request?->getRequestType();
442                if (!empty($type) && (strrpos($type, 'json') !== false)) {
443                    $this->request->addHeader('Content-Type', $type);
444                } else {
445                    $this->request->addHeader('Content-Type', Request::JSON);
446                }
447            }
448        }
449
450        // Only encode if the data isn't already encoded
451        if (!((is_string($jsonContent) && (json_decode($jsonContent) !== false)) && (json_last_error() == JSON_ERROR_NONE))) {
452            $this->dataContent = json_encode($jsonContent, JSON_PRETTY_PRINT);
453        } else {
454            $this->dataContent = $jsonContent;
455        }
456
457        return $this;
458    }
459
460    /**
461     * Method to prepare XML data content
462     *
463     * @return Data
464     */
465    public function prepareXml(): Data
466    {
467        if ($this->hasRawData()) {
468            $xmlContent = $this->getRawData();
469        } else {
470            $xmlData    = $this->data;
471            $xmlContent = '';
472
473            // Check for XML files
474            foreach ($xmlData as $xmlDatum) {
475                $xmlContent .= (isset($xmlDatum['filename']) && isset($xmlDatum['contentType']) &&
476                    str_contains(strtolower($xmlDatum['contentType']), 'xml') && file_exists($xmlDatum['filename'])) ?
477                    file_get_contents($xmlDatum['filename']) : $xmlDatum;
478            }
479        }
480
481        if ($this->hasRequest()) {
482            if ($this->request->hasHeader('Content-Type') && !str_contains(strtolower((string)$this->request->getHeader('Content-Type')), 'xml')) {
483                $this->request->removeHeader('Content-Type');
484            }
485            if (!$this->request->hasHeader('Content-Type')) {
486                $type = $this->request?->getRequestType();
487                if (!empty($type) && (strrpos($type, 'xml') !== false)) {
488                    $this->request->addHeader('Content-Type', $type);
489                } else {
490                    $this->request->addHeader('Content-Type', Request::XML);
491                }
492            }
493        }
494
495        $this->dataContent = $xmlContent;
496
497        return $this;
498    }
499
500    /**
501     * Method to prepare multi-part data content
502     *
503     * @return Data
504     */
505    public function prepareMultipart(): Data
506    {
507        $formMessage       = Message::createForm($this->data);
508        $this->dataContent = $formMessage->renderRaw();
509
510        if ($this->hasRequest()) {
511            if ($this->request->hasHeader('Content-Type')) {
512                $this->request->removeHeader('Content-Type');
513            }
514            $this->request->addHeader($formMessage->getHeader('Content-Type'));
515        }
516
517        return $this;
518    }
519
520    /**
521     * Get raw data
522     *
523     * @return ?string
524     */
525    public function getRawData(): ?string
526    {
527        return $this->data[self::POP_CLIENT_REQUEST_RAW_DATA] ?? null;
528    }
529
530    /**
531     * Has raw data
532     *
533     * @return bool
534     */
535    public function hasRawData(): bool
536    {
537        return (count($this->data) == 1) && isset($this->data[self::POP_CLIENT_REQUEST_RAW_DATA]);
538    }
539
540    /**
541     * Get raw data length
542     *
543     * @param  bool $mb
544     * @return int
545     */
546    public function getRawDataLength(bool $mb = false): int
547    {
548        return ($mb) ? mb_strlen((string)$this->getRawData()) : strlen((string)$this->getRawData());
549    }
550
551    /**
552     * Get data array
553     *
554     * @return array
555     */
556    public function toArray(): array
557    {
558        return $this->data;
559    }
560
561    /**
562     * Get common mime types
563     *
564     * @return array
565     */
566    public static function getMimeTypes(): array
567    {
568        return static::$mimeTypes;
569    }
570
571    /**
572     * Has mime type
573     *
574     * @param  string $ext
575     * @return bool
576     */
577    public static function hasMimeType(string $ext): bool
578    {
579        return isset(static::$mimeTypes[$ext]);
580    }
581
582    /**
583     * Get mime type
584     *
585     * @param  string $ext
586     * @return ?string
587     */
588    public static function getMimeType(string $ext): ?string
589    {
590        return static::$mimeTypes[$ext] ?? null;
591    }
592
593    /**
594     * Get mime type
595     *
596     * @param  string $filename
597     * @return string
598     */
599    public static function getMimeTypeFromFilename(string $filename): string
600    {
601        $info = pathinfo($filename);
602
603        return (isset($info['extension']) && isset(self::$mimeTypes[$info['extension']])) ?
604            self::$mimeTypes[$info['extension']] : self::$defaultMimeType;
605    }
606
607    /**
608     * Get default mime type
609     *
610     * @return string
611     */
612    public static function getDefaultMimeType(): string
613    {
614        return static::$defaultMimeType;
615    }
616
617}