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)) { |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
55 | throw new InvalidArgumentException('Upload file size must be an integer'); |
||||
56 | } |
||||
57 | 58 | ||||
58 | 6 | if (null !== $clientFilename && !is_string($clientFilename)) { |
|||
0 ignored issues
–
show
|
|||||
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)) { |
|||
0 ignored issues
–
show
|
|||||
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
It seems like
$this->file can also be of type null ; however, parameter $filename of fopen() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 | } |