Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.78% |
128 / 141 |
|
86.21% |
25 / 29 |
CRAP | |
0.00% |
0 / 1 |
Data | |
90.78% |
128 / 141 |
|
86.21% |
25 / 29 |
105.53 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
setData | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
addData | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
hasData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
removeData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
removeAllData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setRequest | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDataContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDataContentLength | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
hasDataContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPrepared | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
prepare | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
16.10 | |||
reset | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
prepareUrlEncoded | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
prepareJson | |
77.27% |
17 / 22 |
|
0.00% |
0 / 1 |
20.39 | |||
prepareXml | |
72.22% |
13 / 18 |
|
0.00% |
0 / 1 |
16.62 | |||
prepareMultipart | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getRawData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasRawData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getRawDataLength | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
toArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMimeTypes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasMimeType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMimeType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMimeTypeFromFilename | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getDefaultMimeType | |
100.00% |
1 / 1 |
|
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 | */ |
14 | namespace Pop\Http\Client; |
15 | |
16 | use Pop\Http\HttpFilterableTrait; |
17 | use Pop\Mime\Message; |
18 | use 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 | */ |
30 | class 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 | } |