Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.22% covered (success)
99.22%
128 / 129
96.55% covered (success)
96.55%
28 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
99.22% covered (success)
99.22%
128 / 129
96.55% covered (success)
96.55%
28 / 29
85
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
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
11
 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
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
13
 prepareXml
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 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 (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\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@nolainteractive.com>
26 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
27 * @license    http://www.popphp.org/license     New BSD License
28 * @version    5.2.0
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        switch ($this->request?->getRequestType()) {
329            case Request::URLENCODED:
330                $this->prepareUrlEncoded();
331                break;
332            case Request::JSON:
333                $this->prepareJson();
334                break;
335            case Request::XML:
336                $this->prepareXml();
337                break;
338            case Request::MULTIPART:
339                $this->prepareMultipart();
340                break;
341            default:
342                if ($this->hasRawData()) {
343                    $this->dataContent = $this->getRawData();
344                } else if ($this->hasData()) {
345                    $this->prepareUrlEncoded();
346                }
347        }
348
349        if (!empty($this->dataContent) && ($this->hasRequest())) {
350            if ($this->request->hasHeader('Content-Length')) {
351                $this->request->removeHeader('Content-Length');
352            }
353            $this->request->addHeader('Content-Length', strlen($this->dataContent));
354        }
355
356        $this->prepared = true;
357
358        return $this;
359    }
360
361    /**
362     * Reset data
363     *
364     * @return Data
365     */
366    public function reset(): Data
367    {
368        $this->dataContent = null;
369        $this->prepared    = false;
370        return $this;
371    }
372
373    /**
374     * Method to prepare URL-encoded data content
375     *
376     * @return Data
377     */
378    public function prepareUrlEncoded(): Data
379    {
380        if (!array_key_exists(self::POP_CLIENT_REQUEST_RAW_DATA, $this->data) && !empty($this->data)) {
381            $data = $this->data;
382            if ($this->hasFilters()) {
383                $data = $this->filter($data);
384            }
385            $this->dataContent = http_build_query($data);
386        } else {
387            $this->dataContent = null;
388        }
389
390        if ($this->hasRequest()) {
391            if ($this->request->hasHeader('Content-Type')) {
392                $this->request->removeHeader('Content-Type');
393            }
394            $this->request->addHeader('Content-Type', Request::URLENCODED);
395        }
396
397        return $this;
398    }
399
400    /**
401     * Method to prepare JSON data content
402     *
403     * @return Data
404     */
405    public function prepareJson(): Data
406    {
407        if ($this->hasRawData()) {
408            $jsonContent = $this->getRawData();
409        } else {
410            $jsonData    = $this->data;
411            $jsonContent = [];
412
413            // Check for JSON files
414            foreach ($jsonData as $jsonDatum) {
415                if (isset($jsonDatum['filename']) && isset($jsonDatum['contentType']) &&
416                    str_contains(strtolower($jsonDatum['contentType']), 'json') && file_exists($jsonDatum['filename'])) {
417                    $jsonContent = array_merge($jsonContent, json_decode(file_get_contents($jsonDatum['filename']), true));
418                }
419            }
420
421            // Else, use JSON data
422            if (empty($jsonContent)) {
423                $jsonContent = $jsonData;
424            }
425        }
426
427        if ($this->hasRequest()) {
428            if ($this->request->hasHeader('Content-Type')) {
429                $this->request->removeHeader('Content-Type');
430            }
431            $this->request->addHeader('Content-Type', Request::JSON);
432        }
433
434        // Only encode if the data isn't already encoded
435        if (!((is_string($jsonContent) && (json_decode($jsonContent) !== false)) && (json_last_error() == JSON_ERROR_NONE))) {
436            $this->dataContent = json_encode($jsonContent, JSON_PRETTY_PRINT);
437        } else {
438            $this->dataContent = $jsonContent;
439        }
440
441        return $this;
442    }
443
444    /**
445     * Method to prepare XML data content
446     *
447     * @return Data
448     */
449    public function prepareXml(): Data
450    {
451        if ($this->hasRawData()) {
452            $xmlContent = $this->getRawData();
453        } else {
454            $xmlData    = $this->data;
455            $xmlContent = '';
456
457            // Check for XML files
458            foreach ($xmlData as $xmlDatum) {
459                $xmlContent .= (isset($xmlDatum['filename']) && isset($xmlDatum['contentType']) &&
460                    str_contains(strtolower($xmlDatum['contentType']), 'xml') && file_exists($xmlDatum['filename'])) ?
461                    file_get_contents($xmlDatum['filename']) : $xmlDatum;
462            }
463        }
464
465        if ($this->hasRequest()) {
466            if ($this->request->hasHeader('Content-Type')) {
467                $this->request->removeHeader('Content-Type');
468            }
469            $this->request->addHeader('Content-Type', Request::XML);
470        }
471
472        $this->dataContent = $xmlContent;
473
474        return $this;
475    }
476
477    /**
478     * Method to prepare multi-part data content
479     *
480     * @return Data
481     */
482    public function prepareMultipart(): Data
483    {
484        $formMessage       = Message::createForm($this->data);
485        $this->dataContent = $formMessage->renderRaw();
486
487        if ($this->hasRequest()) {
488            if ($this->request->hasHeader('Content-Type')) {
489                $this->request->removeHeader('Content-Type');
490            }
491            $this->request->addHeader($formMessage->getHeader('Content-Type'));
492        }
493
494        return $this;
495    }
496
497    /**
498     * Get raw data
499     *
500     * @return ?string
501     */
502    public function getRawData(): ?string
503    {
504        return $this->data[self::POP_CLIENT_REQUEST_RAW_DATA] ?? null;
505    }
506
507    /**
508     * Has raw data
509     *
510     * @return bool
511     */
512    public function hasRawData(): bool
513    {
514        return (count($this->data) == 1) && isset($this->data[self::POP_CLIENT_REQUEST_RAW_DATA]);
515    }
516
517    /**
518     * Get raw data length
519     *
520     * @param  bool $mb
521     * @return int
522     */
523    public function getRawDataLength(bool $mb = false): int
524    {
525        return ($mb) ? mb_strlen((string)$this->getRawData()) : strlen((string)$this->getRawData());
526    }
527
528    /**
529     * Get data array
530     *
531     * @return array
532     */
533    public function toArray(): array
534    {
535        return $this->data;
536    }
537
538    /**
539     * Get common mime types
540     *
541     * @return array
542     */
543    public static function getMimeTypes(): array
544    {
545        return static::$mimeTypes;
546    }
547
548    /**
549     * Has mime type
550     *
551     * @param  string $ext
552     * @return bool
553     */
554    public static function hasMimeType(string $ext): bool
555    {
556        return isset(static::$mimeTypes[$ext]);
557    }
558
559    /**
560     * Get mime type
561     *
562     * @param  string $ext
563     * @return ?string
564     */
565    public static function getMimeType(string $ext): ?string
566    {
567        return static::$mimeTypes[$ext] ?? null;
568    }
569
570    /**
571     * Get mime type
572     *
573     * @param  string $filename
574     * @return string
575     */
576    public static function getMimeTypeFromFilename(string $filename): string
577    {
578        $info = pathinfo($filename);
579
580        return (isset($info['extension']) && isset(self::$mimeTypes[$info['extension']])) ?
581            self::$mimeTypes[$info['extension']] : self::$defaultMimeType;
582    }
583
584    /**
585     * Get default mime type
586     *
587     * @return string
588     */
589    public static function getDefaultMimeType(): string
590    {
591        return static::$defaultMimeType;
592    }
593
594}