Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.40% covered (danger)
0.40%
1 / 249
2.86% covered (danger)
2.86%
1 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
0.40% covered (danger)
0.40%
1 / 249
2.86% covered (danger)
2.86%
1 / 35
14110.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 post
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 header
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 cookie
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 file
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 method
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 protocolVersion
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 host
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 uri
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 queryString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 session
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sessionId
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 isValidSessionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sessionRegenerateId
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 rawHead
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rawBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rawBuffer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseHeadFirstLine
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 parseProtocolVersion
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseHeaders
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 parseGet
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 parsePost
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 parseUploadFiles
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 parseUploadFile
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
342
 createSessionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSidCookie
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __set
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __get
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __isset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __unset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __wakeup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 destroy
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * This file is part of workerman.
4 *
5 * Licensed under The MIT License
6 * For full copyright and license information, please see the MIT-LICENSE.txt
7 * Redistributions of files must retain the above copyright notice.
8 *
9 * @author    walkor<walkor@workerman.net>
10 * @copyright walkor<walkor@workerman.net>
11 * @link      http://www.workerman.net/
12 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
13 */
14
15declare(strict_types=1);
16
17namespace Workerman\Protocols\Http;
18
19use Exception;
20use RuntimeException;
21use Stringable;
22use Workerman\Connection\TcpConnection;
23use Workerman\Protocols\Http;
24use function array_walk_recursive;
25use function bin2hex;
26use function clearstatcache;
27use function count;
28use function explode;
29use function file_put_contents;
30use function is_file;
31use function json_decode;
32use function ltrim;
33use function microtime;
34use function pack;
35use function parse_str;
36use function parse_url;
37use function preg_match;
38use function preg_replace;
39use function strlen;
40use function strpos;
41use function strstr;
42use function strtolower;
43use function substr;
44use function tempnam;
45use function trim;
46use function unlink;
47use function urlencode;
48
49/**
50 * Class Request
51 * @package Workerman\Protocols\Http
52 */
53class Request implements Stringable
54{
55    /**
56     * Connection.
57     *
58     * @var ?TcpConnection
59     */
60    public ?TcpConnection $connection = null;
61
62    /**
63     * @var int
64     */
65    public static int $maxFileUploads = 1024;
66
67    /**
68     * Maximum string length for cache
69     *
70     * @var int
71     */
72    public const MAX_CACHE_STRING_LENGTH = 4096;
73
74    /**
75     * Maximum cache size.
76     *
77     * @var int
78     */
79    public const MAX_CACHE_SIZE = 256;
80
81    /**
82     * Properties.
83     *
84     * @var array
85     */
86    public array $properties = [];
87
88    /**
89     * Request data.
90     *
91     * @var array
92     */
93    protected array $data = [];
94
95    /**
96     * Is safe.
97     *
98     * @var bool
99     */
100    protected bool $isSafe = true;
101
102    /**
103     * Context.
104     *
105     * @var array
106     */
107    public array $context = [];
108
109    /**
110     * Request constructor.
111     *
112     */
113    public function __construct(protected string $buffer) {}
114
115    /**
116     * Get query.
117     *
118     * @param string|null $name
119     * @param mixed $default
120     * @return mixed
121     */
122    public function get(?string $name = null, mixed $default = null): mixed
123    {
124        if (!isset($this->data['get'])) {
125            $this->parseGet();
126        }
127        if (null === $name) {
128            return $this->data['get'];
129        }
130        return $this->data['get'][$name] ?? $default;
131    }
132
133    /**
134     * Get post.
135     *
136     * @param string|null $name
137     * @param mixed $default
138     * @return mixed
139     */
140    public function post(?string $name = null, mixed $default = null): mixed
141    {
142        if (!isset($this->data['post'])) {
143            $this->parsePost();
144        }
145        if (null === $name) {
146            return $this->data['post'];
147        }
148        return $this->data['post'][$name] ?? $default;
149    }
150
151    /**
152     * Get header item by name.
153     *
154     * @param string|null $name
155     * @param mixed $default
156     * @return mixed
157     */
158    public function header(?string $name = null, mixed $default = null): mixed
159    {
160        if (!isset($this->data['headers'])) {
161            $this->parseHeaders();
162        }
163        if (null === $name) {
164            return $this->data['headers'];
165        }
166        $name = strtolower($name);
167        return $this->data['headers'][$name] ?? $default;
168    }
169
170    /**
171     * Get cookie item by name.
172     *
173     * @param string|null $name
174     * @param mixed $default
175     * @return mixed
176     */
177    public function cookie(?string $name = null, mixed $default = null): mixed
178    {
179        if (!isset($this->data['cookie'])) {
180            $cookies = explode(';', $this->header('cookie', ''));
181            $mapped = array();
182
183            foreach ($cookies as $cookie) {
184                $cookie = explode('=', $cookie, 2);
185                if (count($cookie) !== 2) {
186                    continue;
187                }
188                $mapped[trim($cookie[0])] = $cookie[1];
189            }
190            $this->data['cookie'] = $mapped;
191        }
192        if ($name === null) {
193            return $this->data['cookie'];
194        }
195        return $this->data['cookie'][$name] ?? $default;
196    }
197
198    /**
199     * Get upload files.
200     *
201     * @param string|null $name
202     * @return array|null
203     */
204    public function file(?string $name = null): mixed
205    {
206        clearstatcache();
207        if (!empty($this->data['files'])) {
208            array_walk_recursive($this->data['files'], function ($value, $key) {
209                if ($key === 'tmp_name' && !is_file($value)) {
210                    $this->data['files'] = [];
211                }
212            });
213        }
214        if (empty($this->data['files'])) {
215            $this->parsePost();
216        }
217        if (null === $name) {
218            return $this->data['files'];
219        }
220        return $this->data['files'][$name] ?? null;
221    }
222
223    /**
224     * Get method.
225     *
226     * @return string
227     */
228    public function method(): string
229    {
230        if (!isset($this->data['method'])) {
231            $this->parseHeadFirstLine();
232        }
233        return $this->data['method'];
234    }
235
236    /**
237     * Get http protocol version.
238     *
239     * @return string
240     */
241    public function protocolVersion(): string
242    {
243        if (!isset($this->data['protocolVersion'])) {
244            $this->parseProtocolVersion();
245        }
246        return $this->data['protocolVersion'];
247    }
248
249    /**
250     * Get host.
251     *
252     * @param bool $withoutPort
253     * @return string|null
254     */
255    public function host(bool $withoutPort = false): ?string
256    {
257        $host = $this->header('host');
258        if ($host && $withoutPort) {
259            return preg_replace('/:\d{1,5}$/', '', $host);
260        }
261        return $host;
262    }
263
264    /**
265     * Get uri.
266     *
267     * @return string
268     */
269    public function uri(): string
270    {
271        if (!isset($this->data['uri'])) {
272            $this->parseHeadFirstLine();
273        }
274        return $this->data['uri'];
275    }
276
277    /**
278     * Get path.
279     *
280     * @return string
281     */
282    public function path(): string
283    {
284        return $this->data['path'] ??= (string)parse_url($this->uri(), PHP_URL_PATH);
285    }
286
287    /**
288     * Get query string.
289     *
290     * @return string
291     */
292    public function queryString(): string
293    {
294        return $this->data['query_string'] ??= (string)parse_url($this->uri(), PHP_URL_QUERY);
295    }
296
297    /**
298     * Get session.
299     *
300     * @return Session
301     * @throws Exception
302     */
303    public function session(): Session
304    {
305        return $this->context['session'] ??= new Session($this->sessionId());
306    }
307
308    /**
309     * Get/Set session id.
310     *
311     * @param string|null $sessionId
312     * @return string
313     * @throws Exception
314     */
315    public function sessionId(?string $sessionId = null): string
316    {
317        if ($sessionId) {
318            unset($this->context['sid']);
319        }
320        if (!isset($this->context['sid'])) {
321            $sessionName = Session::$name;
322            $sid = $sessionId ? '' : $this->cookie($sessionName);
323            $sid = $this->isValidSessionId($sid) ? $sid : '';
324            if ($sid === '') {
325                if (!$this->connection) {
326                    throw new RuntimeException('Request->session() fail, header already send');
327                }
328                $sid = $sessionId ?: static::createSessionId();
329                $cookieParams = Session::getCookieParams();
330                $this->setSidCookie($sessionName, $sid, $cookieParams);
331            }
332            $this->context['sid'] = $sid;
333        }
334        return $this->context['sid'];
335    }
336
337    /**
338     * Check if session id is valid.
339     *
340     * @param mixed $sessionId
341     * @return bool
342     */
343    public function isValidSessionId(mixed $sessionId): bool
344    {
345        return is_string($sessionId) && preg_match('/^[a-zA-Z0-9"]+$/', $sessionId);
346    }
347
348    /**
349     * Session regenerate id.
350     *
351     * @param bool $deleteOldSession
352     * @return string
353     * @throws Exception
354     */
355    public function sessionRegenerateId(bool $deleteOldSession = false): string
356    {
357        $session = $this->session();
358        $sessionData = $session->all();
359        if ($deleteOldSession) {
360            $session->flush();
361        }
362        $newSid = static::createSessionId();
363        $session = new Session($newSid);
364        $session->put($sessionData);
365        $cookieParams = Session::getCookieParams();
366        $sessionName = Session::$name;
367        $this->setSidCookie($sessionName, $newSid, $cookieParams);
368        return $newSid;
369    }
370
371    /**
372     * Get http raw head.
373     *
374     * @return string
375     */
376    public function rawHead(): string
377    {
378        return $this->data['head'] ??= strstr($this->buffer, "\r\n\r\n", true);
379    }
380
381    /**
382     * Get http raw body.
383     *
384     * @return string
385     */
386    public function rawBody(): string
387    {
388        return substr($this->buffer, strpos($this->buffer, "\r\n\r\n") + 4);
389    }
390
391    /**
392     * Get raw buffer.
393     *
394     * @return string
395     */
396    public function rawBuffer(): string
397    {
398        return $this->buffer;
399    }
400
401    /**
402     * Parse first line of http header buffer.
403     *
404     * @return void
405     */
406    protected function parseHeadFirstLine(): void
407    {
408        $firstLine = strstr($this->buffer, "\r\n", true);
409        $tmp = explode(' ', $firstLine, 3);
410        $this->data['method'] = $tmp[0];
411        $this->data['uri'] = $tmp[1] ?? '/';
412    }
413
414    /**
415     * Parse protocol version.
416     *
417     * @return void
418     */
419    protected function parseProtocolVersion(): void
420    {
421        $firstLine = strstr($this->buffer, "\r\n", true);
422        $httpStr = strstr($firstLine, 'HTTP/');
423        $protocolVersion = $httpStr ? substr($httpStr, 5) : '1.0';
424        $this->data['protocolVersion'] = $protocolVersion;
425    }
426
427    /**
428     * Parse headers.
429     *
430     * @return void
431     */
432    protected function parseHeaders(): void
433    {
434        static $cache = [];
435        $this->data['headers'] = [];
436        $rawHead = $this->rawHead();
437        $endLinePosition = strpos($rawHead, "\r\n");
438        if ($endLinePosition === false) {
439            return;
440        }
441        $headBuffer = substr($rawHead, $endLinePosition + 2);
442        $cacheable = !isset($headBuffer[static::MAX_CACHE_STRING_LENGTH]);
443        if ($cacheable && isset($cache[$headBuffer])) {
444            $this->data['headers'] = $cache[$headBuffer];
445            return;
446        }
447        $headData = explode("\r\n", $headBuffer);
448        foreach ($headData as $content) {
449            if (str_contains($content, ':')) {
450                [$key, $value] = explode(':', $content, 2);
451                $key = strtolower($key);
452                $value = ltrim($value);
453            } else {
454                $key = strtolower($content);
455                $value = '';
456            }
457            if (isset($this->data['headers'][$key])) {
458                $this->data['headers'][$key] = "{$this->data['headers'][$key]},$value";
459            } else {
460                $this->data['headers'][$key] = $value;
461            }
462        }
463        if ($cacheable) {
464            $cache[$headBuffer] = $this->data['headers'];
465            if (count($cache) > static::MAX_CACHE_SIZE) {
466                unset($cache[key($cache)]);
467            }
468        }
469    }
470
471    /**
472     * Parse head.
473     *
474     * @return void
475     */
476    protected function parseGet(): void
477    {
478        static $cache = [];
479        $queryString = $this->queryString();
480        $this->data['get'] = [];
481        if ($queryString === '') {
482            return;
483        }
484        $cacheable = !isset($queryString[static::MAX_CACHE_STRING_LENGTH]);
485        if ($cacheable && isset($cache[$queryString])) {
486            $this->data['get'] = $cache[$queryString];
487            return;
488        }
489        parse_str($queryString, $this->data['get']);
490        if ($cacheable) {
491            $cache[$queryString] = $this->data['get'];
492            if (count($cache) > static::MAX_CACHE_SIZE) {
493                unset($cache[key($cache)]);
494            }
495        }
496    }
497
498    /**
499     * Parse post.
500     *
501     * @return void
502     */
503    protected function parsePost(): void
504    {
505        static $cache = [];
506        $this->data['post'] = $this->data['files'] = [];
507        $contentType = $this->header('content-type', '');
508        if (preg_match('/boundary="?(\S+)"?/', $contentType, $match)) {
509            $httpPostBoundary = '--' . $match[1];
510            $this->parseUploadFiles($httpPostBoundary);
511            return;
512        }
513        $bodyBuffer = $this->rawBody();
514        if ($bodyBuffer === '') {
515            return;
516        }
517        $cacheable = !isset($bodyBuffer[static::MAX_CACHE_STRING_LENGTH]);
518        if ($cacheable && isset($cache[$bodyBuffer])) {
519            $this->data['post'] = $cache[$bodyBuffer];
520            return;
521        }
522        if (preg_match('/\bjson\b/i', $contentType)) {
523            $this->data['post'] = (array)json_decode($bodyBuffer, true);
524        } else {
525            parse_str($bodyBuffer, $this->data['post']);
526        }
527        if ($cacheable) {
528            $cache[$bodyBuffer] = $this->data['post'];
529            if (count($cache) > static::MAX_CACHE_SIZE) {
530                unset($cache[key($cache)]);
531            }
532        }
533    }
534
535    /**
536     * Parse upload files.
537     *
538     * @param string $httpPostBoundary
539     * @return void
540     */
541    protected function parseUploadFiles(string $httpPostBoundary): void
542    {
543        $httpPostBoundary = trim($httpPostBoundary, '"');
544        $buffer = $this->buffer;
545        $postEncodeString = '';
546        $filesEncodeString = '';
547        $files = [];
548        $bodyPosition = strpos($buffer, "\r\n\r\n") + 4;
549        $offset = $bodyPosition + strlen($httpPostBoundary) + 2;
550        $maxCount = static::$maxFileUploads;
551        while ($maxCount-- > 0 && $offset) {
552            $offset = $this->parseUploadFile($httpPostBoundary, $offset, $postEncodeString, $filesEncodeString, $files);
553        }
554        if ($postEncodeString) {
555            parse_str($postEncodeString, $this->data['post']);
556        }
557
558        if ($filesEncodeString) {
559            parse_str($filesEncodeString, $this->data['files']);
560            array_walk_recursive($this->data['files'], function (&$value) use ($files) {
561                $value = $files[$value];
562            });
563        }
564    }
565
566    /**
567     * Parse upload file.
568     *
569     * @param string $boundary
570     * @param int $sectionStartOffset
571     * @param string $postEncodeString
572     * @param string $filesEncodeStr
573     * @param array $files
574     * @return int
575     */
576    protected function parseUploadFile(string $boundary, int $sectionStartOffset, string &$postEncodeString, string &$filesEncodeStr, array &$files): int
577    {
578        $file = [];
579        $boundary = "\r\n$boundary";
580        if (strlen($this->buffer) < $sectionStartOffset) {
581            return 0;
582        }
583        $sectionEndOffset = strpos($this->buffer, $boundary, $sectionStartOffset);
584        if (!$sectionEndOffset) {
585            return 0;
586        }
587        $contentLinesEndOffset = strpos($this->buffer, "\r\n\r\n", $sectionStartOffset);
588        if (!$contentLinesEndOffset || $contentLinesEndOffset + 4 > $sectionEndOffset) {
589            return 0;
590        }
591        $contentLinesStr = substr($this->buffer, $sectionStartOffset, $contentLinesEndOffset - $sectionStartOffset);
592        $contentLines = explode("\r\n", trim($contentLinesStr . "\r\n"));
593        $boundaryValue = substr($this->buffer, $contentLinesEndOffset + 4, $sectionEndOffset - $contentLinesEndOffset - 4);
594        $uploadKey = false;
595        foreach ($contentLines as $contentLine) {
596            if (!strpos($contentLine, ': ')) {
597                return 0;
598            }
599            [$key, $value] = explode(': ', $contentLine);
600            switch (strtolower($key)) {
601
602                case "content-disposition":
603                    // Is file data.
604                    if (preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
605                        $error = 0;
606                        $tmpFile = '';
607                        $fileName = $match[1];
608                        $size = strlen($boundaryValue);
609                        $tmpUploadDir = HTTP::uploadTmpDir();
610                        if (!$tmpUploadDir) {
611                            $error = UPLOAD_ERR_NO_TMP_DIR;
612                        } else if ($boundaryValue === '' && $fileName === '') {
613                            $error = UPLOAD_ERR_NO_FILE;
614                        } else {
615                            $tmpFile = tempnam($tmpUploadDir, 'workerman.upload.');
616                            if ($tmpFile === false || false === file_put_contents($tmpFile, $boundaryValue)) {
617                                $error = UPLOAD_ERR_CANT_WRITE;
618                            }
619                        }
620                        $uploadKey = $fileName;
621                        // Parse upload files.
622                        $file = [...$file, 'name' => $match[2], 'tmp_name' => $tmpFile, 'size' => $size, 'error' => $error, 'full_path' => $match[2]];
623                        $file['type'] ??= '';
624                        break;
625                    }
626                    // Is post field.
627                    // Parse $POST.
628                    if (preg_match('/name="(.*?)"$/', $value, $match)) {
629                        $k = $match[1];
630                        $postEncodeString .= urlencode($k) . "=" . urlencode($boundaryValue) . '&';
631                    }
632                    return $sectionEndOffset + strlen($boundary) + 2;
633                
634                case "content-type":
635                    $file['type'] = trim($value);
636                    break;
637
638                case "webkitrelativepath":
639                    $file['full_path'] = trim($value);
640                    break;
641            }
642        }
643        if ($uploadKey === false) {
644            return 0;
645        }
646        $filesEncodeStr .= urlencode($uploadKey) . '=' . count($files) . '&';
647        $files[] = $file;
648
649        return $sectionEndOffset + strlen($boundary) + 2;
650    }
651
652    /**
653     * Create session id.
654     *
655     * @return string
656     * @throws Exception
657     */
658    public static function createSessionId(): string
659    {
660        return bin2hex(pack('d', microtime(true)) . random_bytes(8));
661    }
662
663    /**
664     * @param string $sessionName
665     * @param string $sid
666     * @param array $cookieParams
667     * @return void
668     */
669    protected function setSidCookie(string $sessionName, string $sid, array $cookieParams): void
670    {
671        if (!$this->connection) {
672            throw new RuntimeException('Request->setSidCookie() fail, header already send');
673        }
674        $this->connection->headers['Set-Cookie'] = [$sessionName . '=' . $sid
675            . (empty($cookieParams['domain']) ? '' : '; Domain=' . $cookieParams['domain'])
676            . (empty($cookieParams['lifetime']) ? '' : '; Max-Age=' . $cookieParams['lifetime'])
677            . (empty($cookieParams['path']) ? '' : '; Path=' . $cookieParams['path'])
678            . (empty($cookieParams['samesite']) ? '' : '; SameSite=' . $cookieParams['samesite'])
679            . (!$cookieParams['secure'] ? '' : '; Secure')
680            . (!$cookieParams['httponly'] ? '' : '; HttpOnly')];
681    }
682
683    /**
684     * __toString.
685     */
686    public function __toString(): string
687    {
688        return $this->buffer;
689    }
690
691    /**
692     * Setter.
693     *
694     * @param string $name
695     * @param mixed $value
696     * @return void
697     */
698    public function __set(string $name, mixed $value): void
699    {
700        $this->properties[$name] = $value;
701    }
702
703    /**
704     * Getter.
705     *
706     * @param string $name
707     * @return mixed
708     */
709    public function __get(string $name): mixed
710    {
711        return $this->properties[$name] ?? null;
712    }
713
714    /**
715     * Isset.
716     *
717     * @param string $name
718     * @return bool
719     */
720    public function __isset(string $name): bool
721    {
722        return isset($this->properties[$name]);
723    }
724
725    /**
726     * Unset.
727     *
728     * @param string $name
729     * @return void
730     */
731    public function __unset(string $name): void
732    {
733        unset($this->properties[$name]);
734    }
735
736    /**
737     * __wakeup.
738     *
739     * @return void
740     */
741    public function __wakeup(): void
742    {
743        $this->isSafe = false;
744    }
745
746    /**
747     * Destroy.
748     *
749     * @return void
750     */
751    public function destroy(): void
752    {
753        if ($this->context) {
754            $this->context  = [];
755        }
756        if ($this->properties) {
757            $this->properties = [];
758        }
759        if (isset($this->data['files']) && $this->isSafe) {
760            clearstatcache();
761            array_walk_recursive($this->data['files'], function ($value, $key) {
762                if ($key === 'tmp_name' && is_file($value)) {
763                    unlink($value);
764                }
765            });
766        }
767    }
768
769}