UploadedFile.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\HttpMessage\Upload;
  12. use Hyperf\HttpMessage\Stream\StandardStream;
  13. use InvalidArgumentException;
  14. use Psr\Http\Message\StreamInterface;
  15. use Psr\Http\Message\UploadedFileInterface;
  16. use RuntimeException;
  17. use SplFileInfo;
  18. use Stringable;
  19. class UploadedFile extends SplFileInfo implements UploadedFileInterface, Stringable
  20. {
  21. /**
  22. * @var int[]
  23. */
  24. private static array $errors = [
  25. UPLOAD_ERR_OK,
  26. UPLOAD_ERR_INI_SIZE,
  27. UPLOAD_ERR_FORM_SIZE,
  28. UPLOAD_ERR_PARTIAL,
  29. UPLOAD_ERR_NO_FILE,
  30. UPLOAD_ERR_NO_TMP_DIR,
  31. UPLOAD_ERR_CANT_WRITE,
  32. UPLOAD_ERR_EXTENSION,
  33. ];
  34. private ?string $tmpFile = null;
  35. private bool $moved = false;
  36. /**
  37. * @var null|string
  38. */
  39. private $mimeType;
  40. /**
  41. * @param int $size The file size
  42. * @param int $error The error associated with the uploaded file
  43. * @param null|string $clientFilename The filename sent by the client
  44. * @param null|string $clientMediaType The media type sent by the client
  45. */
  46. public function __construct(
  47. string $tmpFile,
  48. private int $size,
  49. private int $error,
  50. private ?string $clientFilename = null,
  51. private ?string $clientMediaType = null
  52. ) {
  53. $this->checkError($this->error);
  54. $this->isOk() && $this->tmpFile = $tmpFile;
  55. parent::__construct($tmpFile);
  56. }
  57. public function __toString(): string
  58. {
  59. return json_encode($this->toArray());
  60. }
  61. public function getExtension(): string
  62. {
  63. $clientName = $this->getClientFilename();
  64. $segments = explode('.', $clientName);
  65. return (string) end($segments);
  66. }
  67. public function getMimeType(): string
  68. {
  69. if (is_string($this->mimeType)) {
  70. return $this->mimeType;
  71. }
  72. return $this->mimeType = mime_content_type($this->tmpFile);
  73. }
  74. /**
  75. * Returns whether the file was uploaded successfully.
  76. *
  77. * @return bool True if the file has been uploaded with HTTP and no error occurred
  78. */
  79. public function isValid(): bool
  80. {
  81. $isOk = $this->error === UPLOAD_ERR_OK;
  82. return $isOk && is_uploaded_file($this->getPathname());
  83. }
  84. /**
  85. * Determine if the temp file is moved.
  86. */
  87. public function isMoved(): bool
  88. {
  89. return $this->moved;
  90. }
  91. /**
  92. * Retrieve a stream representing the uploaded file.
  93. * This method MUST return a StreamInterface instance, representing the
  94. * uploaded file. The purpose of this method is to allow utilizing native PHP
  95. * stream functionality to manipulate the file upload, such as
  96. * stream_copy_to_stream() (though the result will need to be decorated in a
  97. * native PHP stream wrapper to work with such functions).
  98. * If the moveTo() method has been called previously, this method MUST raise
  99. * an exception.
  100. *
  101. * @return StreamInterface stream representation of the uploaded file
  102. * @throws RuntimeException in cases when no stream is available or can be
  103. * created
  104. */
  105. public function getStream(): StreamInterface
  106. {
  107. if ($this->moved) {
  108. throw new RuntimeException('uploaded file is moved');
  109. }
  110. return StandardStream::create(fopen($this->tmpFile, 'r+'));
  111. }
  112. /**
  113. * Move the uploaded file to a new location.
  114. * Use this method as an alternative to move_uploaded_file(). This method is
  115. * guaranteed to work in both SAPI and non-SAPI environments.
  116. * Implementations must determine which environment they are in, and use the
  117. * appropriate method (move_uploaded_file(), rename(), or a stream
  118. * operation) to perform the operation.
  119. * $targetPath may be an absolute path, or a relative path. If it is a
  120. * relative path, resolution should be the same as used by PHP's rename()
  121. * function.
  122. * The original file or stream MUST be removed on completion.
  123. * If this method is called more than once, any subsequent calls MUST raise
  124. * an exception.
  125. * When used in an SAPI environment where $_FILES is populated, when writing
  126. * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
  127. * used to ensure permissions and upload status are verified correctly.
  128. * If you wish to move to a stream, use getStream(), as SAPI operations
  129. * cannot guarantee writing to stream destinations.
  130. *
  131. * @see http://php.net/is_uploaded_file
  132. * @see http://php.net/move_uploaded_file
  133. * @param string $targetPath path to which to move the uploaded file
  134. * @throws InvalidArgumentException if the $targetPath specified is invalid
  135. * @throws RuntimeException on any error during the move operation, or on
  136. * the second or subsequent call to the method
  137. */
  138. public function moveTo($targetPath): void
  139. {
  140. $this->validateActive();
  141. if (! $this->isStringNotEmpty($targetPath)) {
  142. throw new InvalidArgumentException('Invalid path provided for move operation');
  143. }
  144. if ($this->tmpFile) {
  145. $this->moved = php_sapi_name() == 'cli' ? rename($this->tmpFile, $targetPath) : move_uploaded_file($this->tmpFile, $targetPath);
  146. }
  147. if (! $this->moved) {
  148. throw new RuntimeException(sprintf('Uploaded file could not be move to %s', $targetPath));
  149. }
  150. }
  151. /**
  152. * Retrieve the file size.
  153. * Implementations SHOULD return the value stored in the "size" key of
  154. * the file in the $_FILES array if available, as PHP calculates this based
  155. * on the actual size transmitted.
  156. *
  157. * @return int the file size in bytes or null if unknown
  158. */
  159. public function getSize(): int
  160. {
  161. return $this->size;
  162. }
  163. /**
  164. * Retrieve the error associated with the uploaded file.
  165. * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
  166. * If the file was uploaded successfully, this method MUST return
  167. * UPLOAD_ERR_OK.
  168. * Implementations SHOULD return the value stored in the "error" key of
  169. * the file in the $_FILES array.
  170. *
  171. * @see http://php.net/manual/en/features.file-upload.errors.php
  172. * @return int one of PHP's UPLOAD_ERR_XXX constants
  173. */
  174. public function getError(): int
  175. {
  176. return $this->error;
  177. }
  178. /**
  179. * Retrieve the filename sent by the client.
  180. * Do not trust the value returned by this method. A client could send
  181. * a malicious filename with the intention to corrupt or hack your
  182. * application.
  183. * Implementations SHOULD return the value stored in the "name" key of
  184. * the file in the $_FILES array.
  185. *
  186. * @return null|string the filename sent by the client or null if none
  187. * was provided
  188. */
  189. public function getClientFilename(): ?string
  190. {
  191. return $this->clientFilename;
  192. }
  193. /**
  194. * Retrieve the media type sent by the client.
  195. * Do not trust the value returned by this method. A client could send
  196. * a malicious media type with the intention to corrupt or hack your
  197. * application.
  198. * Implementations SHOULD return the value stored in the "type" key of
  199. * the file in the $_FILES array.
  200. *
  201. * @return null|string the media type sent by the client or null if none
  202. * was provided
  203. */
  204. public function getClientMediaType(): ?string
  205. {
  206. return $this->clientMediaType;
  207. }
  208. public function toArray(): array
  209. {
  210. return [
  211. 'name' => $this->getClientFilename(),
  212. 'type' => $this->getClientMediaType(),
  213. 'tmp_file' => $this->tmpFile,
  214. 'error' => $this->getError(),
  215. 'size' => $this->getSize(),
  216. ];
  217. }
  218. private function checkError(int $error): void
  219. {
  220. if (in_array($error, UploadedFile::$errors) === false) {
  221. throw new InvalidArgumentException('Invalid error status for UploadedFile');
  222. }
  223. }
  224. private function isStringNotEmpty($param): bool
  225. {
  226. return is_string($param) && empty($param) === false;
  227. }
  228. /**
  229. * Return true if there is no upload error.
  230. */
  231. private function isOk(): bool
  232. {
  233. return $this->error === UPLOAD_ERR_OK;
  234. }
  235. /**
  236. * @throws RuntimeException if is moved or not ok
  237. */
  238. private function validateActive()
  239. {
  240. if ($this->isOk() === false) {
  241. throw new RuntimeException('Cannot retrieve stream due to upload error');
  242. }
  243. if ($this->isMoved()) {
  244. throw new RuntimeException('Cannot retrieve stream after it has already been moved');
  245. }
  246. }
  247. }