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