1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | |||
4 | namespace PTS\Psr7; |
||
5 | |||
6 | use InvalidArgumentException; |
||
7 | use Psr\Http\Message\StreamInterface; |
||
8 | use Psr\Http\Message\UploadedFileInterface; |
||
9 | use RuntimeException; |
||
10 | use Throwable; |
||
11 | use function fopen; |
||
12 | use function is_int; |
||
13 | use function is_resource; |
||
14 | use function is_string; |
||
15 | use function move_uploaded_file; |
||
16 | use function rename; |
||
17 | use function sprintf; |
||
18 | |||
19 | final class UploadedFile implements UploadedFileInterface |
||
20 | { |
||
21 | /** @var array */ |
||
22 | private const ERRORS = [ |
||
23 | \UPLOAD_ERR_OK => 1, |
||
24 | \UPLOAD_ERR_INI_SIZE => 1, |
||
25 | \UPLOAD_ERR_FORM_SIZE => 1, |
||
26 | \UPLOAD_ERR_PARTIAL => 1, |
||
27 | \UPLOAD_ERR_NO_FILE => 1, |
||
28 | \UPLOAD_ERR_NO_TMP_DIR => 1, |
||
29 | \UPLOAD_ERR_CANT_WRITE => 1, |
||
30 | \UPLOAD_ERR_EXTENSION => 1, |
||
31 | ]; |
||
32 | |||
33 | protected ?string $clientFilename = null; |
||
34 | protected ?string $clientMediaType = null; |
||
35 | protected int $error; |
||
36 | private ?string $file = null; |
||
37 | private bool $moved = false; |
||
38 | private int $size; |
||
39 | private ?StreamInterface $stream = null; |
||
40 | |||
41 | /** |
||
42 | * @param StreamInterface|string|resource $streamOrFile |
||
43 | * @param int $size |
||
44 | * @param int $errorStatus |
||
45 | * @param string|null $clientFilename |
||
46 | * @param string|null $clientMediaType |
||
47 | 68 | */ |
|
48 | public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null) |
||
49 | 68 | { |
|
50 | 9 | if (false === is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) { |
|
51 | throw new InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.'); |
||
52 | } |
||
53 | 59 | ||
54 | 1 | if (false === is_int($size)) { |
|
55 | throw new InvalidArgumentException('Upload file size must be an integer'); |
||
56 | } |
||
57 | 58 | ||
58 | 6 | if (null !== $clientFilename && !is_string($clientFilename)) { |
|
59 | throw new InvalidArgumentException('Upload file client filename must be a string or null'); |
||
60 | } |
||
61 | 52 | ||
62 | 6 | if (null !== $clientMediaType && !is_string($clientMediaType)) { |
|
63 | throw new InvalidArgumentException('Upload file client media type must be a string or null'); |
||
64 | } |
||
65 | 46 | ||
66 | 46 | $this->error = $errorStatus; |
|
67 | 46 | $this->size = $size; |
|
68 | 46 | $this->clientFilename = $clientFilename; |
|
69 | $this->clientMediaType = $clientMediaType; |
||
70 | 46 | ||
71 | if (\UPLOAD_ERR_OK === $this->error) { |
||
72 | 25 | // Depending on the value set file or stream variable. |
|
73 | 3 | if (is_string($streamOrFile) && '' !== $streamOrFile) { |
|
74 | 22 | $this->file = $streamOrFile; |
|
75 | 1 | } elseif (is_resource($streamOrFile)) { |
|
76 | 21 | $this->stream = Stream::create($streamOrFile); |
|
77 | 14 | } elseif ($streamOrFile instanceof StreamInterface) { |
|
78 | $this->stream = $streamOrFile; |
||
79 | 7 | } else { |
|
80 | throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile'); |
||
81 | } |
||
82 | 39 | } |
|
83 | } |
||
84 | |||
85 | /** |
||
86 | * @throws RuntimeException if is moved or not ok |
||
87 | 30 | */ |
|
88 | private function validateActive(): void |
||
89 | 30 | { |
|
90 | 14 | if (\UPLOAD_ERR_OK !== $this->error) { |
|
91 | throw new RuntimeException('Cannot retrieve stream due to upload error'); |
||
92 | } |
||
93 | 16 | ||
94 | 2 | if ($this->moved) { |
|
95 | throw new RuntimeException('Cannot retrieve stream after it has already been moved'); |
||
96 | 16 | } |
|
97 | } |
||
98 | 14 | ||
99 | public function getStream(): StreamInterface |
||
100 | 14 | { |
|
101 | $this->validateActive(); |
||
102 | 7 | ||
103 | 6 | if ($this->stream instanceof StreamInterface) { |
|
104 | return $this->stream; |
||
105 | } |
||
106 | 1 | ||
107 | try { |
||
108 | 1 | return Stream::create(fopen($this->file, 'r')); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
109 | } catch (Throwable $e) { |
||
110 | throw new RuntimeException(sprintf('The file "%s" cannot be opened.', $this->file)); |
||
111 | 20 | } |
|
112 | } |
||
113 | 20 | ||
114 | public function moveTo($targetPath): void |
||
115 | 13 | { |
|
116 | 8 | $this->validateActive(); |
|
117 | |||
118 | if (!is_string($targetPath) || '' === $targetPath) { |
||
119 | 5 | throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); |
|
120 | 1 | } |
|
121 | 1 | ||
122 | if (null !== $this->file) { |
||
123 | $this->moved = 'cli' === \PHP_SAPI |
||
124 | 1 | ? rename($this->file, $targetPath) |
|
125 | : move_uploaded_file($this->file, $targetPath); |
||
126 | |||
127 | if (false === $this->moved) { |
||
128 | 1 | throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath)); |
|
129 | } |
||
130 | |||
131 | 4 | return; |
|
132 | 4 | } |
|
133 | 4 | ||
134 | $stream = $this->getStream(); |
||
135 | if ($stream->isSeekable()) { |
||
136 | $stream->rewind(); |
||
137 | } |
||
138 | 4 | ||
139 | 3 | // Copy the contents of a stream into another stream until end-of-file. |
|
140 | 3 | try { |
|
141 | $dest = Stream::create(fopen($targetPath, 'w')); |
||
142 | while (!$stream->eof()) { |
||
143 | if (!$dest->write($stream->read(1048576))) { |
||
144 | break; |
||
145 | 3 | } |
|
146 | 1 | } |
|
147 | 1 | ||
148 | $this->moved = true; |
||
149 | 3 | } catch (Throwable $throwable) { |
|
150 | throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath), 0, $throwable); |
||
151 | 2 | } |
|
152 | } |
||
153 | 2 | ||
154 | public function getSize(): int |
||
155 | { |
||
156 | 7 | return $this->size; |
|
157 | } |
||
158 | 7 | ||
159 | public function getError(): int |
||
160 | { |
||
161 | 1 | return $this->error; |
|
162 | } |
||
163 | 1 | ||
164 | public function getClientFilename(): ?string |
||
165 | { |
||
166 | 1 | return $this->clientFilename; |
|
167 | } |
||
168 | 1 | ||
169 | public function getClientMediaType(): ?string |
||
170 | 1 | { |
|
171 | return $this->clientMediaType; |
||
172 | } |
||
173 | } |