FormFlashFile.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <?php
  2. /**
  3. * @package Grav\Framework\Form
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Form;
  9. use Grav\Common\Security;
  10. use Grav\Common\Utils;
  11. use Grav\Framework\Psr7\Stream;
  12. use InvalidArgumentException;
  13. use JsonSerializable;
  14. use Psr\Http\Message\StreamInterface;
  15. use Psr\Http\Message\UploadedFileInterface;
  16. use RuntimeException;
  17. use function copy;
  18. use function fopen;
  19. use function is_string;
  20. use function sprintf;
  21. /**
  22. * Class FormFlashFile
  23. * @package Grav\Framework\Form
  24. */
  25. class FormFlashFile implements UploadedFileInterface, JsonSerializable
  26. {
  27. /** @var string */
  28. private $id;
  29. /** @var string */
  30. private $field;
  31. /** @var bool */
  32. private $moved = false;
  33. /** @var array */
  34. private $upload;
  35. /** @var FormFlash */
  36. private $flash;
  37. /**
  38. * FormFlashFile constructor.
  39. * @param string $field
  40. * @param array $upload
  41. * @param FormFlash $flash
  42. */
  43. public function __construct(string $field, array $upload, FormFlash $flash)
  44. {
  45. $this->id = $flash->getId() ?: $flash->getUniqueId();
  46. $this->field = $field;
  47. $this->upload = $upload;
  48. $this->flash = $flash;
  49. $tmpFile = $this->getTmpFile();
  50. if (!$tmpFile && $this->isOk()) {
  51. $this->upload['error'] = \UPLOAD_ERR_NO_FILE;
  52. }
  53. if (!isset($this->upload['size'])) {
  54. $this->upload['size'] = $tmpFile && $this->isOk() ? filesize($tmpFile) : 0;
  55. }
  56. }
  57. /**
  58. * @return StreamInterface
  59. */
  60. public function getStream()
  61. {
  62. $this->validateActive();
  63. $tmpFile = $this->getTmpFile();
  64. if (null === $tmpFile) {
  65. throw new RuntimeException('No temporary file');
  66. }
  67. $resource = fopen($tmpFile, 'rb');
  68. if (false === $resource) {
  69. throw new RuntimeException('No temporary file');
  70. }
  71. return Stream::create($resource);
  72. }
  73. /**
  74. * @param string $targetPath
  75. * @return void
  76. */
  77. public function moveTo($targetPath)
  78. {
  79. $this->validateActive();
  80. if (!is_string($targetPath) || empty($targetPath)) {
  81. throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
  82. }
  83. $tmpFile = $this->getTmpFile();
  84. if (null === $tmpFile) {
  85. throw new RuntimeException('No temporary file');
  86. }
  87. $this->moved = copy($tmpFile, $targetPath);
  88. if (false === $this->moved) {
  89. throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath));
  90. }
  91. $filename = $this->getClientFilename();
  92. if ($filename) {
  93. $this->flash->removeFile($filename, $this->field);
  94. }
  95. }
  96. public function getId(): string
  97. {
  98. return $this->id;
  99. }
  100. /**
  101. * @return string
  102. */
  103. public function getField(): string
  104. {
  105. return $this->field;
  106. }
  107. /**
  108. * @return int
  109. */
  110. public function getSize()
  111. {
  112. return $this->upload['size'];
  113. }
  114. /**
  115. * @return int
  116. */
  117. public function getError()
  118. {
  119. return $this->upload['error'] ?? \UPLOAD_ERR_OK;
  120. }
  121. /**
  122. * @return string
  123. */
  124. public function getClientFilename()
  125. {
  126. return $this->upload['name'] ?? 'unknown';
  127. }
  128. /**
  129. * @return string
  130. */
  131. public function getClientMediaType()
  132. {
  133. return $this->upload['type'] ?? 'application/octet-stream';
  134. }
  135. /**
  136. * @return bool
  137. */
  138. public function isMoved(): bool
  139. {
  140. return $this->moved;
  141. }
  142. /**
  143. * @return array
  144. */
  145. public function getMetaData(): array
  146. {
  147. if (isset($this->upload['crop'])) {
  148. return ['crop' => $this->upload['crop']];
  149. }
  150. return [];
  151. }
  152. /**
  153. * @return string
  154. */
  155. public function getDestination()
  156. {
  157. return $this->upload['path'] ?? '';
  158. }
  159. /**
  160. * @return array
  161. */
  162. #[\ReturnTypeWillChange]
  163. public function jsonSerialize()
  164. {
  165. return $this->upload;
  166. }
  167. /**
  168. * @return void
  169. */
  170. public function checkXss(): void
  171. {
  172. $tmpFile = $this->getTmpFile();
  173. $mime = $this->getClientMediaType();
  174. if (Utils::contains($mime, 'svg', false)) {
  175. $response = Security::detectXssFromSvgFile($tmpFile);
  176. if ($response) {
  177. throw new RuntimeException(sprintf('SVG file XSS check failed on %s', $response));
  178. }
  179. }
  180. }
  181. /**
  182. * @return string|null
  183. */
  184. public function getTmpFile(): ?string
  185. {
  186. $tmpName = $this->upload['tmp_name'] ?? null;
  187. if (!$tmpName) {
  188. return null;
  189. }
  190. $tmpFile = $this->flash->getTmpDir() . '/' . $tmpName;
  191. return file_exists($tmpFile) ? $tmpFile : null;
  192. }
  193. /**
  194. * @return array
  195. */
  196. #[\ReturnTypeWillChange]
  197. public function __debugInfo()
  198. {
  199. return [
  200. 'id:private' => $this->id,
  201. 'field:private' => $this->field,
  202. 'moved:private' => $this->moved,
  203. 'upload:private' => $this->upload,
  204. ];
  205. }
  206. /**
  207. * @return void
  208. * @throws RuntimeException if is moved or not ok
  209. */
  210. private function validateActive(): void
  211. {
  212. if (!$this->isOk()) {
  213. throw new RuntimeException('Cannot retrieve stream due to upload error');
  214. }
  215. if ($this->moved) {
  216. throw new RuntimeException('Cannot retrieve stream after it has already been moved');
  217. }
  218. if (!$this->getTmpFile()) {
  219. throw new RuntimeException('Cannot retrieve stream as the file is missing');
  220. }
  221. }
  222. /**
  223. * @return bool return true if there is no upload error
  224. */
  225. private function isOk(): bool
  226. {
  227. return \UPLOAD_ERR_OK === $this->getError();
  228. }
  229. }