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 string|null
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     *
148     * @return bool
149     */
150    private function isStringOrNull($param)
151    {
152        return in_array(gettype($param), ['string', 'NULL']);
153    }
154
155    /**
156     * @param mixed $param
157     *
158     * @return bool
159     */
160    private function isStringNotEmpty($param)
161    {
162        return is_string($param) && false === empty($param);
163    }
164
165    /**
166     * @param string|null $clientFilename
167     *
168     * @throws InvalidArgumentException
169     */
170    private function setClientFilename($clientFilename)
171    {
172        if (false === $this->isStringOrNull($clientFilename)) {
173            throw new InvalidArgumentException(
174                'Upload file client filename must be a string or null'
175            );
176        }
177
178        $this->clientFilename = $clientFilename;
179    }
180
181    /**
182     * @param string|null $clientMediaType
183     *
184     * @throws InvalidArgumentException
185     */
186    private function setClientMediaType($clientMediaType)
187    {
188        if (false === $this->isStringOrNull($clientMediaType)) {
189            throw new InvalidArgumentException(
190                'Upload file client media type must be a string or null'
191            );
192        }
193
194        $this->clientMediaType = $clientMediaType;
195    }
196
197    /**
198     * Return true if there is no upload error
199     *
200     * @return bool
201     */
202    private function isOk()
203    {
204        return $this->error === UPLOAD_ERR_OK;
205    }
206
207    /**
208     * @return bool
209     */
210    public function isMoved()
211    {
212        return $this->moved;
213    }
214
215    /**
216     * @throws RuntimeException if is moved or not ok
217     */
218    private function validateActive()
219    {
220        if (false === $this->isOk()) {
221            throw new RuntimeException('Cannot retrieve stream due to upload error');
222        }
223
224        if ($this->isMoved()) {
225            throw new RuntimeException('Cannot retrieve stream after it has already been moved');
226        }
227    }
228
229    /**
230     * {@inheritdoc}
231     *
232     * @throws RuntimeException if the upload was not successful.
233     */
234    public function getStream()
235    {
236        $this->validateActive();
237
238        if ($this->stream instanceof StreamInterface) {
239            return $this->stream;
240        }
241
242        return new LazyOpenStream($this->file, 'r+');
243    }
244
245    /**
246     * {@inheritdoc}
247     *
248     * @see http://php.net/is_uploaded_file
249     * @see http://php.net/move_uploaded_file
250     *
251     * @param string $targetPath Path to which to move the uploaded file.
252     *
253     * @throws RuntimeException         if the upload was not successful.
254     * @throws InvalidArgumentException if the $path specified is invalid.
255     * @throws RuntimeException         on any error during the move operation, or on
256     *                                  the second or subsequent call to the method.
257     */
258    public function moveTo($targetPath)
259    {
260        $this->validateActive();
261
262        if (false === $this->isStringNotEmpty($targetPath)) {
263            throw new InvalidArgumentException(
264                'Invalid path provided for move operation; must be a non-empty string'
265            );
266        }
267
268        if ($this->file) {
269            $this->moved = php_sapi_name() == 'cli'
270                ? rename($this->file, $targetPath)
271                : move_uploaded_file($this->file, $targetPath);
272        } else {
273            Utils::copyToStream(
274                $this->getStream(),
275                new LazyOpenStream($targetPath, 'w')
276            );
277
278            $this->moved = true;
279        }
280
281        if (false === $this->moved) {
282            throw new RuntimeException(
283                sprintf('Uploaded file could not be moved to %s', $targetPath)
284            );
285        }
286    }
287
288    /**
289     * {@inheritdoc}
290     *
291     * @return int|null The file size in bytes or null if unknown.
292     */
293    public function getSize()
294    {
295        return $this->size;
296    }
297
298    /**
299     * {@inheritdoc}
300     *
301     * @see http://php.net/manual/en/features.file-upload.errors.php
302     *
303     * @return int One of PHP's UPLOAD_ERR_XXX constants.
304     */
305    public function getError()
306    {
307        return $this->error;
308    }
309
310    /**
311     * {@inheritdoc}
312     *
313     * @return string|null The filename sent by the client or null if none
314     *                     was provided.
315     */
316    public function getClientFilename()
317    {
318        return $this->clientFilename;
319    }
320
321    /**
322     * {@inheritdoc}
323     */
324    public function getClientMediaType()
325    {
326        return $this->clientMediaType;
327    }
328}
329