1<?php
2
3namespace GuzzleHttp\Psr7;
4
5use InvalidArgumentException;
6use Psr\Http\Message\StreamInterface;
7use Psr\Http\Message\UploadedFileInterface;
8use RuntimeException;
9
10class UploadedFile implements UploadedFileInterface
11{
12    /**
13     * @var int[]
14     */
15    private static $errors = [
16        UPLOAD_ERR_OK,
17        UPLOAD_ERR_INI_SIZE,
18        UPLOAD_ERR_FORM_SIZE,
19        UPLOAD_ERR_PARTIAL,
20        UPLOAD_ERR_NO_FILE,
21        UPLOAD_ERR_NO_TMP_DIR,
22        UPLOAD_ERR_CANT_WRITE,
23        UPLOAD_ERR_EXTENSION,
24    ];
25
26    /**
27     * @var string
28     */
29    private $clientFilename;
30
31    /**
32     * @var string
33     */
34    private $clientMediaType;
35
36    /**
37     * @var int
38     */
39    private $error;
40
41    /**
42     * @var null|string
43     */
44    private $file;
45
46    /**
47     * @var bool
48     */
49    private $moved = false;
50
51    /**
52     * @var int
53     */
54    private $size;
55
56    /**
57     * @var StreamInterface|null
58     */
59    private $stream;
60
61    /**
62     * @param StreamInterface|string|resource $streamOrFile
63     * @param int $size
64     * @param int $errorStatus
65     * @param string|null $clientFilename
66     * @param string|null $clientMediaType
67     */
68    public function __construct(
69        $streamOrFile,
70        $size,
71        $errorStatus,
72        $clientFilename = null,
73        $clientMediaType = null
74    ) {
75        $this->setError($errorStatus);
76        $this->setSize($size);
77        $this->setClientFilename($clientFilename);
78        $this->setClientMediaType($clientMediaType);
79
80        if ($this->isOk()) {
81            $this->setStreamOrFile($streamOrFile);
82        }
83    }
84
85    /**
86     * Depending on the value set file or stream variable
87     *
88     * @param mixed $streamOrFile
89     *
90     * @throws InvalidArgumentException
91     */
92    private function setStreamOrFile($streamOrFile)
93    {
94        if (is_string($streamOrFile)) {
95            $this->file = $streamOrFile;
96        } elseif (is_resource($streamOrFile)) {
97            $this->stream = new Stream($streamOrFile);
98        } elseif ($streamOrFile instanceof StreamInterface) {
99            $this->stream = $streamOrFile;
100        } else {
101            throw new InvalidArgumentException(
102                'Invalid stream or file provided for UploadedFile'
103            );
104        }
105    }
106
107    /**
108     * @param int $error
109     *
110     * @throws InvalidArgumentException
111     */
112    private function setError($error)
113    {
114        if (false === is_int($error)) {
115            throw new InvalidArgumentException(
116                'Upload file error status must be an integer'
117            );
118        }
119
120        if (false === in_array($error, UploadedFile::$errors)) {
121            throw new InvalidArgumentException(
122                'Invalid error status for UploadedFile'
123            );
124        }
125
126        $this->error = $error;
127    }
128
129    /**
130     * @param int $size
131     *
132     * @throws InvalidArgumentException
133     */
134    private function setSize($size)
135    {
136        if (false === is_int($size)) {
137            throw new InvalidArgumentException(
138                'Upload file size must be an integer'
139            );
140        }
141
142        $this->size = $size;
143    }
144
145    /**
146     * @param mixed $param
147     * @return boolean
148     */
149    private function isStringOrNull($param)
150    {
151        return in_array(gettype($param), ['string', 'NULL']);
152    }
153
154    /**
155     * @param mixed $param
156     * @return boolean
157     */
158    private function isStringNotEmpty($param)
159    {
160        return is_string($param) && false === empty($param);
161    }
162
163    /**
164     * @param string|null $clientFilename
165     *
166     * @throws InvalidArgumentException
167     */
168    private function setClientFilename($clientFilename)
169    {
170        if (false === $this->isStringOrNull($clientFilename)) {
171            throw new InvalidArgumentException(
172                'Upload file client filename must be a string or null'
173            );
174        }
175
176        $this->clientFilename = $clientFilename;
177    }
178
179    /**
180     * @param string|null $clientMediaType
181     *
182     * @throws InvalidArgumentException
183     */
184    private function setClientMediaType($clientMediaType)
185    {
186        if (false === $this->isStringOrNull($clientMediaType)) {
187            throw new InvalidArgumentException(
188                'Upload file client media type must be a string or null'
189            );
190        }
191
192        $this->clientMediaType = $clientMediaType;
193    }
194
195    /**
196     * Return true if there is no upload error
197     *
198     * @return boolean
199     */
200    private function isOk()
201    {
202        return $this->error === UPLOAD_ERR_OK;
203    }
204
205    /**
206     * @return boolean
207     */
208    public function isMoved()
209    {
210        return $this->moved;
211    }
212
213    /**
214     * @throws RuntimeException if is moved or not ok
215     */
216    private function validateActive()
217    {
218        if (false === $this->isOk()) {
219            throw new RuntimeException('Cannot retrieve stream due to upload error');
220        }
221
222        if ($this->isMoved()) {
223            throw new RuntimeException('Cannot retrieve stream after it has already been moved');
224        }
225    }
226
227    /**
228     * {@inheritdoc}
229     *
230     * @throws RuntimeException if the upload was not successful.
231     */
232    public function getStream()
233    {
234        $this->validateActive();
235
236        if ($this->stream instanceof StreamInterface) {
237            return $this->stream;
238        }
239
240        return new LazyOpenStream($this->file, 'r+');
241    }
242
243    /**
244     * {@inheritdoc}
245     *
246     * @see http://php.net/is_uploaded_file
247     * @see http://php.net/move_uploaded_file
248     *
249     * @param string $targetPath Path to which to move the uploaded file.
250     *
251     * @throws RuntimeException if the upload was not successful.
252     * @throws InvalidArgumentException if the $path specified is invalid.
253     * @throws RuntimeException on any error during the move operation, or on
254     *     the second or subsequent call to the method.
255     */
256    public function moveTo($targetPath)
257    {
258        $this->validateActive();
259
260        if (false === $this->isStringNotEmpty($targetPath)) {
261            throw new InvalidArgumentException(
262                'Invalid path provided for move operation; must be a non-empty string'
263            );
264        }
265
266        if ($this->file) {
267            $this->moved = php_sapi_name() == 'cli'
268                ? rename($this->file, $targetPath)
269                : move_uploaded_file($this->file, $targetPath);
270        } else {
271            Utils::copyToStream(
272                $this->getStream(),
273                new LazyOpenStream($targetPath, 'w')
274            );
275
276            $this->moved = true;
277        }
278
279        if (false === $this->moved) {
280            throw new RuntimeException(
281                sprintf('Uploaded file could not be moved to %s', $targetPath)
282            );
283        }
284    }
285
286    /**
287     * {@inheritdoc}
288     *
289     * @return int|null The file size in bytes or null if unknown.
290     */
291    public function getSize()
292    {
293        return $this->size;
294    }
295
296    /**
297     * {@inheritdoc}
298     *
299     * @see http://php.net/manual/en/features.file-upload.errors.php
300     * @return int One of PHP's UPLOAD_ERR_XXX constants.
301     */
302    public function getError()
303    {
304        return $this->error;
305    }
306
307    /**
308     * {@inheritdoc}
309     *
310     * @return string|null The filename sent by the client or null if none
311     *     was provided.
312     */
313    public function getClientFilename()
314    {
315        return $this->clientFilename;
316    }
317
318    /**
319     * {@inheritdoc}
320     */
321    public function getClientMediaType()
322    {
323        return $this->clientMediaType;
324    }
325}
326