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