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