3 * @see https://github.com/zendframework/zend-diactoros for the canonical source repository
4 * @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
5 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
8 namespace Zend\Diactoros;
10 use InvalidArgumentException;
11 use Psr\Http\Message\StreamInterface;
12 use Psr\Http\Message\UploadedFileInterface;
21 use function is_resource;
22 use function is_string;
23 use function is_writable;
24 use function move_uploaded_file;
29 use const UPLOAD_ERR_CANT_WRITE;
30 use const UPLOAD_ERR_EXTENSION;
31 use const UPLOAD_ERR_FORM_SIZE;
32 use const UPLOAD_ERR_INI_SIZE;
33 use const UPLOAD_ERR_NO_FILE;
34 use const UPLOAD_ERR_NO_TMP_DIR;
35 use const UPLOAD_ERR_OK;
36 use const UPLOAD_ERR_PARTIAL;
38 class UploadedFile implements UploadedFileInterface
40 const ERROR_MESSAGES = [
41 UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
42 UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
43 UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was '
44 . 'specified in the HTML form',
45 UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
46 UPLOAD_ERR_NO_FILE => 'No file was uploaded',
47 UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
48 UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
49 UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
55 private $clientFilename;
60 private $clientMediaType;
75 private $moved = false;
83 * @var null|StreamInterface
88 * @param string|resource $streamOrFile
90 * @param int $errorStatus
91 * @param string|null $clientFilename
92 * @param string|null $clientMediaType
93 * @throws InvalidArgumentException
95 public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
97 if ($errorStatus === UPLOAD_ERR_OK) {
98 if (is_string($streamOrFile)) {
99 $this->file = $streamOrFile;
101 if (is_resource($streamOrFile)) {
102 $this->stream = new Stream($streamOrFile);
105 if (! $this->file && ! $this->stream) {
106 if (! $streamOrFile instanceof StreamInterface) {
107 throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile');
109 $this->stream = $streamOrFile;
113 if (! is_int($size)) {
114 throw new InvalidArgumentException('Invalid size provided for UploadedFile; must be an int');
118 if (! is_int($errorStatus)
122 throw new InvalidArgumentException(
123 'Invalid error status for UploadedFile; must be an UPLOAD_ERR_* constant'
126 $this->error = $errorStatus;
128 if (null !== $clientFilename && ! is_string($clientFilename)) {
129 throw new InvalidArgumentException(
130 'Invalid client filename provided for UploadedFile; must be null or a string'
133 $this->clientFilename = $clientFilename;
135 if (null !== $clientMediaType && ! is_string($clientMediaType)) {
136 throw new InvalidArgumentException(
137 'Invalid client media type provided for UploadedFile; must be null or a string'
140 $this->clientMediaType = $clientMediaType;
145 * @throws \RuntimeException if the upload was not successful.
147 public function getStream()
149 if ($this->error !== UPLOAD_ERR_OK) {
150 throw new RuntimeException(sprintf(
151 'Cannot retrieve stream due to upload error: %s',
152 self::ERROR_MESSAGES[$this->error]
157 throw new RuntimeException('Cannot retrieve stream after it has already been moved');
160 if ($this->stream instanceof StreamInterface) {
161 return $this->stream;
164 $this->stream = new Stream($this->file);
165 return $this->stream;
171 * @see http://php.net/is_uploaded_file
172 * @see http://php.net/move_uploaded_file
173 * @param string $targetPath Path to which to move the uploaded file.
174 * @throws \RuntimeException if the upload was not successful.
175 * @throws \InvalidArgumentException if the $path specified is invalid.
176 * @throws \RuntimeException on any error during the move operation, or on
177 * the second or subsequent call to the method.
179 public function moveTo($targetPath)
182 throw new RuntimeException('Cannot move file; already moved!');
185 if ($this->error !== UPLOAD_ERR_OK) {
186 throw new RuntimeException(sprintf(
187 'Cannot retrieve stream due to upload error: %s',
188 self::ERROR_MESSAGES[$this->error]
192 if (! is_string($targetPath) || empty($targetPath)) {
193 throw new InvalidArgumentException(
194 'Invalid path provided for move operation; must be a non-empty string'
198 $targetDirectory = dirname($targetPath);
199 if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
200 throw new RuntimeException(sprintf(
201 'The target directory `%s` does not exists or is not writable',
208 case (empty($sapi) || 0 === strpos($sapi, 'cli') || ! $this->file):
209 // Non-SAPI environment, or no filename present
210 $this->writeFile($targetPath);
213 // SAPI environment, with file present
214 if (false === move_uploaded_file($this->file, $targetPath)) {
215 throw new RuntimeException('Error occurred while moving uploaded file');
226 * @return int|null The file size in bytes or null if unknown.
228 public function getSize()
236 * @see http://php.net/manual/en/features.file-upload.errors.php
237 * @return int One of PHP's UPLOAD_ERR_XXX constants.
239 public function getError()
247 * @return string|null The filename sent by the client or null if none
250 public function getClientFilename()
252 return $this->clientFilename;
258 public function getClientMediaType()
260 return $this->clientMediaType;
264 * Write internal stream to given path
266 * @param string $path
268 private function writeFile($path)
270 $handle = fopen($path, 'wb+');
271 if (false === $handle) {
272 throw new RuntimeException('Unable to write to designated path');
275 $stream = $this->getStream();
277 while (! $stream->eof()) {
278 fwrite($handle, $stream->read(4096));