This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This file is part of Carpediem\JSend, a JSend Response object. |
||
5 | * |
||
6 | * @copyright Carpe Diem. All rights reserved |
||
7 | * @license MIT See LICENSE.md at the root of the project for more info |
||
8 | */ |
||
9 | |||
10 | declare(strict_types=1); |
||
11 | |||
12 | namespace Carpediem\JSend; |
||
13 | |||
14 | use JsonSerializable; |
||
15 | use TypeError; |
||
16 | use const JSON_ERROR_NONE; |
||
17 | use const JSON_HEX_AMP; |
||
18 | use const JSON_HEX_APOS; |
||
19 | use const JSON_HEX_QUOT; |
||
20 | use function array_filter; |
||
21 | use function array_merge; |
||
22 | use function header; |
||
23 | use function is_array; |
||
24 | use function is_object; |
||
25 | use function is_scalar; |
||
26 | use function json_decode; |
||
27 | use function json_encode; |
||
28 | use function json_last_error; |
||
29 | use function json_last_error_msg; |
||
30 | use function method_exists; |
||
31 | use function preg_match; |
||
32 | use function sprintf; |
||
33 | use function strlen; |
||
34 | use function trim; |
||
35 | |||
36 | /** |
||
37 | * A Immutable Value Object Class to represent a JSend object. |
||
38 | */ |
||
39 | final class JSend implements JsonSerializable |
||
40 | { |
||
41 | const STATUS_SUCCESS = 'success'; |
||
42 | |||
43 | const STATUS_ERROR = 'error'; |
||
44 | |||
45 | const STATUS_FAIL = 'fail'; |
||
46 | |||
47 | /** |
||
48 | * JSend status. |
||
49 | * |
||
50 | * @var string |
||
51 | */ |
||
52 | private $status; |
||
53 | |||
54 | /** |
||
55 | * JSend Data. |
||
56 | * |
||
57 | * @var array |
||
58 | */ |
||
59 | private $data; |
||
60 | |||
61 | /** |
||
62 | * JSend Error Message. |
||
63 | * |
||
64 | * @var string|null |
||
65 | */ |
||
66 | private $errorMessage; |
||
67 | |||
68 | /** |
||
69 | * JSend Error Code. |
||
70 | * |
||
71 | * @var int|null |
||
72 | */ |
||
73 | private $errorCode; |
||
74 | |||
75 | /** |
||
76 | * Returns a new instance from a JSON string. |
||
77 | * |
||
78 | * @throws Exception If the string can not be decode |
||
79 | */ |
||
80 | 9 | public static function fromJSON($json, int $depth = 512, int $options = 0): self |
|
81 | { |
||
82 | 9 | if ($json instanceof JsonSerializable) { |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
83 | 3 | return self::fromArray((array) $json->jsonSerialize()); |
|
84 | } |
||
85 | |||
86 | 9 | if (!is_scalar($json) && !method_exists($json, '__toString')) { |
|
87 | 3 | throw new TypeError('The json argument must be a string, a stringable object or an object implementing the JsonSerializable interface'); |
|
0 ignored issues
–
show
The call to
TypeError::__construct() has too many arguments starting with 'The json argument must ...Serializable interface' .
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. In this case you can add the ![]() |
|||
88 | } |
||
89 | |||
90 | 6 | $raw = json_decode((string) $json, true, $depth, $options); |
|
91 | 6 | if (JSON_ERROR_NONE === json_last_error()) { |
|
92 | 3 | return static::fromArray((array) $raw); |
|
93 | } |
||
94 | |||
95 | 3 | throw new Exception(sprintf('Unable to decode the submitted JSON string: %s', json_last_error_msg())); |
|
96 | } |
||
97 | |||
98 | /** |
||
99 | * Returns a new instance from an array. |
||
100 | */ |
||
101 | 6 | public static function fromArray(array $arr): self |
|
102 | { |
||
103 | 6 | return new self($arr['status'] ?? '', $arr['data'] ?? null, $arr['message'] ?? null, $arr['code'] ?? null); |
|
104 | } |
||
105 | |||
106 | /** |
||
107 | * Returns a successful JSend object with the specified data. |
||
108 | * |
||
109 | * @param null|mixed $data |
||
110 | */ |
||
111 | 102 | public static function success($data = null): self |
|
112 | { |
||
113 | 102 | return new self(self::STATUS_SUCCESS, $data); |
|
114 | } |
||
115 | |||
116 | /** |
||
117 | * Returns a failed JSend object with the specified data. |
||
118 | * |
||
119 | * @param null|mixed $data |
||
120 | */ |
||
121 | 3 | public static function fail($data = null): self |
|
122 | { |
||
123 | 3 | return new self(self::STATUS_FAIL, $data); |
|
124 | } |
||
125 | |||
126 | /** |
||
127 | * Returns a error JSend object with the specified error message and error code. |
||
128 | * |
||
129 | * @param null|mixed $data |
||
130 | */ |
||
131 | 15 | public static function error($errorMessage, int $errorCode = null, $data = null): self |
|
132 | { |
||
133 | 15 | return new self(self::STATUS_ERROR, $data, $errorMessage, $errorCode); |
|
134 | } |
||
135 | |||
136 | /** |
||
137 | * {@inheritdoc} |
||
138 | */ |
||
139 | 3 | public static function __set_state(array $prop) |
|
140 | { |
||
141 | 3 | return new self($prop['status'], $prop['data'], $prop['errorMessage'], $prop['errorCode']); |
|
142 | } |
||
143 | |||
144 | /** |
||
145 | * New Instance. |
||
146 | * |
||
147 | * @param null|mixed $data |
||
148 | * @param null|mixed $errorMessage |
||
149 | */ |
||
150 | 102 | private function __construct( |
|
151 | string $status, |
||
152 | $data = null, |
||
153 | $errorMessage = null, |
||
154 | int $errorCode = null |
||
155 | ) { |
||
156 | 102 | $this->status = $this->filterStatus($status); |
|
157 | 102 | $this->data = $this->filterData($data); |
|
158 | 102 | list($this->errorMessage, $this->errorCode) = $this->filterError($errorMessage, $errorCode); |
|
159 | 102 | } |
|
160 | |||
161 | /** |
||
162 | * Filter and Validate the JSend Status. |
||
163 | * |
||
164 | * @throws Exception If the status value does not conform to JSend Spec. |
||
165 | */ |
||
166 | 102 | private function filterStatus(string $status): string |
|
167 | { |
||
168 | 102 | static $res = [self::STATUS_SUCCESS => 1, self::STATUS_ERROR => 1, self::STATUS_FAIL => 1]; |
|
169 | 102 | if (isset($res[$status])) { |
|
170 | 102 | return $status; |
|
171 | } |
||
172 | |||
173 | 3 | throw new Exception('The given status does not conform to Jsend specification'); |
|
174 | } |
||
175 | |||
176 | /** |
||
177 | * Filter and Validate the JSend Data. |
||
178 | * |
||
179 | * @param mixed $data The data can be |
||
180 | * <ul> |
||
181 | * <li>An Array |
||
182 | * <li>A JsonSerializable object |
||
183 | * <li>null |
||
184 | * </ul> |
||
185 | * |
||
186 | * @throws Exception If the input does not conform to one of the valid type |
||
187 | */ |
||
188 | 102 | private function filterData($data): array |
|
189 | { |
||
190 | 102 | if (null === $data) { |
|
191 | 15 | return []; |
|
192 | } |
||
193 | |||
194 | 102 | if (is_array($data)) { |
|
195 | 102 | return $data; |
|
196 | } |
||
197 | |||
198 | 9 | if (!$data instanceof JsonSerializable) { |
|
0 ignored issues
–
show
|
|||
199 | 3 | throw new TypeError('The data must be an array, a JsonSerializable object or null'); |
|
0 ignored issues
–
show
The call to
TypeError::__construct() has too many arguments starting with 'The data must be an arr...lizable object or null' .
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. In this case you can add the ![]() |
|||
200 | } |
||
201 | |||
202 | 6 | if (is_array($res = $data->jsonSerialize())) { |
|
203 | 3 | return $res; |
|
204 | } |
||
205 | |||
206 | 3 | throw new Exception(sprintf('The JsonSerializable object must return an array %s returned', is_object($res) ? get_class($res) : gettype($res))); |
|
207 | } |
||
208 | |||
209 | /** |
||
210 | * Filter and Validate the JSend Error properties. |
||
211 | */ |
||
212 | 102 | private function filterError($errorMessage, int $errorCode = null): array |
|
213 | { |
||
214 | 102 | if (self::STATUS_ERROR === $this->status) { |
|
215 | 15 | return [$this->filterErrorMessage($errorMessage), $errorCode]; |
|
216 | } |
||
217 | |||
218 | 102 | return [null, null]; |
|
219 | } |
||
220 | |||
221 | /** |
||
222 | * Validate a string. |
||
223 | * |
||
224 | * @throws Exception If the data value is not a empty string |
||
225 | */ |
||
226 | 15 | private function filterErrorMessage($str): string |
|
227 | { |
||
228 | 15 | if (!is_scalar($str) && !(is_object($str) && method_exists($str, '__toString'))) { |
|
229 | 3 | throw new Exception('The error message must be a scalar or a object implementing the __toString method.'); |
|
230 | } |
||
231 | |||
232 | 12 | $str = (string) $str; |
|
233 | 12 | if ('' !== trim($str)) { |
|
234 | 9 | return $str; |
|
235 | } |
||
236 | |||
237 | 3 | throw new Exception('The error message can not be empty.'); |
|
238 | } |
||
239 | |||
240 | /** |
||
241 | * Returns the status. |
||
242 | */ |
||
243 | 42 | public function getStatus(): string |
|
244 | { |
||
245 | 42 | return $this->status; |
|
246 | } |
||
247 | |||
248 | /** |
||
249 | * Returns the data. |
||
250 | */ |
||
251 | 39 | public function getData(): array |
|
252 | { |
||
253 | 39 | return $this->data; |
|
254 | } |
||
255 | |||
256 | /** |
||
257 | * Returns the error message. |
||
258 | * |
||
259 | * @return null|string |
||
260 | */ |
||
261 | 15 | public function getErrorMessage() |
|
262 | { |
||
263 | 15 | return $this->errorMessage; |
|
264 | } |
||
265 | |||
266 | /** |
||
267 | * Returns the error code. |
||
268 | * |
||
269 | * @return null|int |
||
270 | */ |
||
271 | 18 | public function getErrorCode() |
|
272 | { |
||
273 | 18 | return $this->errorCode; |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Returns whether the status is success. |
||
278 | */ |
||
279 | 9 | public function isSuccess(): bool |
|
280 | { |
||
281 | 9 | return self::STATUS_SUCCESS === $this->status; |
|
282 | } |
||
283 | |||
284 | /** |
||
285 | * Returns whether the status is fail. |
||
286 | */ |
||
287 | 9 | public function isFail(): bool |
|
288 | { |
||
289 | 9 | return self::STATUS_FAIL === $this->status; |
|
290 | } |
||
291 | |||
292 | /** |
||
293 | * Returns whether the status is error. |
||
294 | */ |
||
295 | 15 | public function isError(): bool |
|
296 | { |
||
297 | 15 | return self::STATUS_ERROR === $this->status; |
|
298 | } |
||
299 | |||
300 | /** |
||
301 | * {@inheritdoc} |
||
302 | */ |
||
303 | 9 | public function jsonSerialize() |
|
304 | { |
||
305 | 9 | return $this->toArray(); |
|
306 | } |
||
307 | |||
308 | /** |
||
309 | * Returns the array representation. |
||
310 | */ |
||
311 | 57 | public function toArray(): array |
|
312 | { |
||
313 | 57 | $arr = ['status' => $this->status, 'data' => $this->data ?: null]; |
|
314 | 57 | if (self::STATUS_ERROR !== $this->status) { |
|
315 | 39 | return $arr; |
|
316 | } |
||
317 | |||
318 | 18 | $filter = function ($value): bool { |
|
319 | 18 | return null !== $value; |
|
320 | 18 | }; |
|
321 | |||
322 | 18 | $arr['message'] = (string) $this->errorMessage; |
|
323 | 18 | $arr['code'] = $this->errorCode; |
|
324 | |||
325 | 18 | return array_filter($arr, $filter); |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * {@inheritdoc} |
||
330 | */ |
||
331 | 48 | public function __toString() |
|
332 | { |
||
333 | 48 | return json_encode($this->toArray(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); |
|
334 | } |
||
335 | |||
336 | /** |
||
337 | * {@inheritdoc} |
||
338 | */ |
||
339 | 3 | public function __debugInfo() |
|
340 | { |
||
341 | 3 | return $this->toArray(); |
|
342 | } |
||
343 | |||
344 | /** |
||
345 | * Output all the data of the JSend object. |
||
346 | * |
||
347 | * @param array $headers Optional headers to add to the response |
||
348 | * |
||
349 | * @return int Returns the number of characters read from the JSend object |
||
350 | * and passed throught to the output |
||
351 | */ |
||
352 | 12 | public function send(array $headers = []): int |
|
353 | { |
||
354 | 12 | $body = $this->__toString(); |
|
355 | 12 | $length = strlen($body); |
|
356 | 12 | $headers = $this->filterHeaders(array_merge([ |
|
357 | 12 | 'Content-Type' => 'application/json;charset=utf-8', |
|
358 | 12 | 'Content-Length' => (string) $length, |
|
359 | 12 | ], $headers)); |
|
360 | |||
361 | 3 | foreach ($headers as $header) { |
|
362 | 3 | header($header); |
|
363 | } |
||
364 | 3 | echo $body; |
|
365 | |||
366 | 3 | return $length; |
|
367 | } |
||
368 | |||
369 | /** |
||
370 | * Filter Submitted Headers. |
||
371 | * |
||
372 | * @param array $headers a Collection of key/value headers |
||
373 | */ |
||
374 | 12 | private function filterHeaders(array $headers): array |
|
375 | { |
||
376 | 12 | $formattedHeaders = []; |
|
377 | 12 | foreach ($headers as $name => $value) { |
|
378 | 12 | $formattedHeaders[] = $this->validateHeaderName($name).': '.$this->validateHeaderValue($value); |
|
379 | } |
||
380 | |||
381 | 3 | return $formattedHeaders; |
|
382 | } |
||
383 | |||
384 | /** |
||
385 | * Validate Header name. |
||
386 | * |
||
387 | * @throws Exception if the header name is invalid |
||
388 | */ |
||
389 | 12 | private function validateHeaderName(string $name): string |
|
390 | { |
||
391 | 12 | if (preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) { |
|
392 | 12 | return $name; |
|
393 | } |
||
394 | |||
395 | 3 | throw new Exception(sprintf('Invalid header name: %s', $name)); |
|
396 | } |
||
397 | |||
398 | /** |
||
399 | * Validate Header value. |
||
400 | * |
||
401 | * @throws Exception if the header value is invalid |
||
402 | */ |
||
403 | 12 | private function validateHeaderValue(string $value): string |
|
404 | { |
||
405 | 12 | if (!preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value) |
|
406 | 12 | && !preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value) |
|
407 | ) { |
||
408 | 12 | return $value; |
|
409 | } |
||
410 | |||
411 | 6 | throw new Exception(sprintf('Invalid header value: %s', $value)); |
|
412 | } |
||
413 | |||
414 | /** |
||
415 | * Returns an instance with the specified status. |
||
416 | * |
||
417 | * This method MUST retain the state of the current instance, and return |
||
418 | * an instance that contains the specified status. |
||
419 | */ |
||
420 | 6 | public function withStatus(string $status): self |
|
421 | { |
||
422 | 6 | $status = $this->filterStatus($status); |
|
423 | 6 | if ($status === $this->status) { |
|
424 | 3 | return $this; |
|
425 | } |
||
426 | |||
427 | 3 | return new self($status, $this->data, $this->errorMessage, $this->errorCode); |
|
428 | } |
||
429 | |||
430 | /** |
||
431 | * Returns an instance with the specified data. |
||
432 | * |
||
433 | * This method MUST retain the state of the current instance, and return |
||
434 | * an instance that contains the specified data. |
||
435 | */ |
||
436 | 6 | public function withData($data): self |
|
437 | { |
||
438 | 6 | $data = $this->filterData($data); |
|
439 | 6 | if ($data === $this->data) { |
|
440 | 3 | return $this; |
|
441 | } |
||
442 | |||
443 | 3 | $clone = clone $this; |
|
444 | 3 | $clone->data = $data; |
|
445 | |||
446 | 3 | return $clone; |
|
447 | } |
||
448 | |||
449 | /** |
||
450 | * Returns an instance with the specified error message and error code. |
||
451 | * |
||
452 | * This method MUST retain the state of the current instance, and return |
||
453 | * an instance that contains the specified error message and error code. |
||
454 | */ |
||
455 | 6 | public function withError($errorMessage, int $errorCode = null): self |
|
456 | { |
||
457 | 6 | list($errorMessage, $errorCode) = $this->filterError($errorMessage, $errorCode); |
|
458 | 6 | if ($this->isError() && $errorMessage === $this->errorMessage && $errorCode === $this->errorCode) { |
|
459 | 3 | return $this; |
|
460 | } |
||
461 | |||
462 | 3 | $clone = clone $this; |
|
463 | 3 | $clone->errorMessage = $errorMessage; |
|
464 | 3 | $clone->errorCode = $errorCode; |
|
465 | 3 | $clone->status = self::STATUS_ERROR; |
|
466 | |||
467 | 3 | return $clone; |
|
468 | } |
||
469 | } |
||
470 |