1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use InvalidArgumentException; 8use Psr\Http\Message\StreamInterface; 9use Psr\Http\Message\UploadedFileInterface; 10use RuntimeException; 11 12class UploadedFile implements UploadedFileInterface 13{ 14 private const ERRORS = [ 15 UPLOAD_ERR_OK, 16 UPLOAD_ERR_INI_SIZE, 17 UPLOAD_ERR_FORM_SIZE, 18 UPLOAD_ERR_PARTIAL, 19 UPLOAD_ERR_NO_FILE, 20 UPLOAD_ERR_NO_TMP_DIR, 21 UPLOAD_ERR_CANT_WRITE, 22 UPLOAD_ERR_EXTENSION, 23 ]; 24 25 /** 26 * @var string|null 27 */ 28 private $clientFilename; 29 30 /** 31 * @var string|null 32 */ 33 private $clientMediaType; 34 35 /** 36 * @var int 37 */ 38 private $error; 39 40 /** 41 * @var string|null 42 */ 43 private $file; 44 45 /** 46 * @var bool 47 */ 48 private $moved = false; 49 50 /** 51 * @var int|null 52 */ 53 private $size; 54 55 /** 56 * @var StreamInterface|null 57 */ 58 private $stream; 59 60 /** 61 * @param StreamInterface|string|resource $streamOrFile 62 */ 63 public function __construct( 64 $streamOrFile, 65 ?int $size, 66 int $errorStatus, 67 string $clientFilename = null, 68 string $clientMediaType = null 69 ) { 70 $this->setError($errorStatus); 71 $this->size = $size; 72 $this->clientFilename = $clientFilename; 73 $this->clientMediaType = $clientMediaType; 74 75 if ($this->isOk()) { 76 $this->setStreamOrFile($streamOrFile); 77 } 78 } 79 80 /** 81 * Depending on the value set file or stream variable 82 * 83 * @param StreamInterface|string|resource $streamOrFile 84 * 85 * @throws InvalidArgumentException 86 */ 87 private function setStreamOrFile($streamOrFile): void 88 { 89 if (is_string($streamOrFile)) { 90 $this->file = $streamOrFile; 91 } elseif (is_resource($streamOrFile)) { 92 $this->stream = new Stream($streamOrFile); 93 } elseif ($streamOrFile instanceof StreamInterface) { 94 $this->stream = $streamOrFile; 95 } else { 96 throw new InvalidArgumentException( 97 'Invalid stream or file provided for UploadedFile' 98 ); 99 } 100 } 101 102 /** 103 * @throws InvalidArgumentException 104 */ 105 private function setError(int $error): void 106 { 107 if (false === in_array($error, UploadedFile::ERRORS, true)) { 108 throw new InvalidArgumentException( 109 'Invalid error status for UploadedFile' 110 ); 111 } 112 113 $this->error = $error; 114 } 115 116 private static function isStringNotEmpty($param): bool 117 { 118 return is_string($param) && false === empty($param); 119 } 120 121 /** 122 * Return true if there is no upload error 123 */ 124 private function isOk(): bool 125 { 126 return $this->error === UPLOAD_ERR_OK; 127 } 128 129 public function isMoved(): bool 130 { 131 return $this->moved; 132 } 133 134 /** 135 * @throws RuntimeException if is moved or not ok 136 */ 137 private function validateActive(): void 138 { 139 if (false === $this->isOk()) { 140 throw new RuntimeException('Cannot retrieve stream due to upload error'); 141 } 142 143 if ($this->isMoved()) { 144 throw new RuntimeException('Cannot retrieve stream after it has already been moved'); 145 } 146 } 147 148 public function getStream(): StreamInterface 149 { 150 $this->validateActive(); 151 152 if ($this->stream instanceof StreamInterface) { 153 return $this->stream; 154 } 155 156 /** @var string $file */ 157 $file = $this->file; 158 159 return new LazyOpenStream($file, 'r+'); 160 } 161 162 public function moveTo($targetPath): void 163 { 164 $this->validateActive(); 165 166 if (false === self::isStringNotEmpty($targetPath)) { 167 throw new InvalidArgumentException( 168 'Invalid path provided for move operation; must be a non-empty string' 169 ); 170 } 171 172 if ($this->file) { 173 $this->moved = PHP_SAPI === 'cli' 174 ? rename($this->file, $targetPath) 175 : move_uploaded_file($this->file, $targetPath); 176 } else { 177 Utils::copyToStream( 178 $this->getStream(), 179 new LazyOpenStream($targetPath, 'w') 180 ); 181 182 $this->moved = true; 183 } 184 185 if (false === $this->moved) { 186 throw new RuntimeException( 187 sprintf('Uploaded file could not be moved to %s', $targetPath) 188 ); 189 } 190 } 191 192 public function getSize(): ?int 193 { 194 return $this->size; 195 } 196 197 public function getError(): int 198 { 199 return $this->error; 200 } 201 202 public function getClientFilename(): ?string 203 { 204 return $this->clientFilename; 205 } 206 207 public function getClientMediaType(): ?string 208 { 209 return $this->clientMediaType; 210 } 211} 212