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 try { 118 return Stream::create(\fopen($this->file, 'r')); 119 } catch (\Throwable $e) { 120 throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $this->file)); 121 } 122 } 123 124 public function moveTo($targetPath): void 125 { 126 $this->validateActive(); 127 128 if (!\is_string($targetPath) || '' === $targetPath) { 129 throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); 130 } 131 132 if (null !== $this->file) { 133 $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath); 134 } else { 135 $stream = $this->getStream(); 136 if ($stream->isSeekable()) { 137 $stream->rewind(); 138 } 139 140 try { 141 // Copy the contents of a stream into another stream until end-of-file. 142 $dest = Stream::create(\fopen($targetPath, 'w')); 143 } catch (\Throwable $e) { 144 throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $targetPath)); 145 } 146 147 while (!$stream->eof()) { 148 if (!$dest->write($stream->read(1048576))) { 149 break; 150 } 151 } 152 153 $this->moved = true; 154 } 155 156 if (false === $this->moved) { 157 throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s"', $targetPath)); 158 } 159 } 160 161 public function getSize(): int 162 { 163 return $this->size; 164 } 165 166 public function getError(): int 167 { 168 return $this->error; 169 } 170 171 public function getClientFilename(): ?string 172 { 173 return $this->clientFilename; 174 } 175 176 public function getClientMediaType(): ?string 177 { 178 return $this->clientMediaType; 179 } 180} 181