FormFlash.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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 Exception;
  10. use Grav\Common\Filesystem\Folder;
  11. use Grav\Common\Grav;
  12. use Grav\Common\User\Interfaces\UserInterface;
  13. use Grav\Common\Utils;
  14. use Grav\Framework\Form\Interfaces\FormFlashInterface;
  15. use Psr\Http\Message\UploadedFileInterface;
  16. use RocketTheme\Toolbox\File\YamlFile;
  17. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  18. use RuntimeException;
  19. use function func_get_args;
  20. use function is_array;
  21. /**
  22. * Class FormFlash
  23. * @package Grav\Framework\Form
  24. */
  25. class FormFlash implements FormFlashInterface
  26. {
  27. /** @var bool */
  28. protected $exists;
  29. /** @var string */
  30. protected $id;
  31. /** @var string */
  32. protected $sessionId;
  33. /** @var string */
  34. protected $uniqueId;
  35. /** @var string */
  36. protected $formName;
  37. /** @var string */
  38. protected $url;
  39. /** @var array|null */
  40. protected $user;
  41. /** @var int */
  42. protected $createdTimestamp;
  43. /** @var int */
  44. protected $updatedTimestamp;
  45. /** @var array|null */
  46. protected $data;
  47. /** @var array */
  48. protected $files;
  49. /** @var array */
  50. protected $uploadedFiles;
  51. /** @var string[] */
  52. protected $uploadObjects;
  53. /** @var string */
  54. protected $folder;
  55. /**
  56. * @inheritDoc
  57. */
  58. public function __construct($config)
  59. {
  60. // Backwards compatibility with Grav 1.6 plugins.
  61. if (!is_array($config)) {
  62. user_error(__CLASS__ . '::' . __FUNCTION__ . '($sessionId, $uniqueId, $formName) is deprecated since Grav 1.6.11, use $config parameter instead', E_USER_DEPRECATED);
  63. $args = func_get_args();
  64. $config = [
  65. 'session_id' => $args[0],
  66. 'unique_id' => $args[1] ?? null,
  67. 'form_name' => $args[2] ?? null,
  68. ];
  69. $config = array_filter($config, static function ($val) {
  70. return $val !== null;
  71. });
  72. }
  73. $this->id = $config['id'] ?? '';
  74. $this->sessionId = $config['session_id'] ?? '';
  75. $this->uniqueId = $config['unique_id'] ?? '';
  76. $this->setUser($config['user'] ?? null);
  77. $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
  78. /** @var UniformResourceLocator $locator */
  79. $locator = Grav::instance()['locator'];
  80. $this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;
  81. $this->init($this->loadStoredForm(), $config);
  82. }
  83. /**
  84. * @param array|null $data
  85. * @param array $config
  86. */
  87. protected function init(?array $data, array $config): void
  88. {
  89. if (null === $data) {
  90. $this->exists = false;
  91. $this->formName = $config['form_name'] ?? '';
  92. $this->url = '';
  93. $this->createdTimestamp = $this->updatedTimestamp = time();
  94. $this->files = [];
  95. } else {
  96. $this->exists = true;
  97. $this->formName = $data['form'] ?? $config['form_name'] ?? '';
  98. $this->url = $data['url'] ?? '';
  99. $this->user = $data['user'] ?? null;
  100. $this->updatedTimestamp = $data['timestamps']['updated'] ?? time();
  101. $this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp;
  102. $this->data = $data['data'] ?? null;
  103. $this->files = $data['files'] ?? [];
  104. }
  105. }
  106. /**
  107. * Load raw flex flash data from the filesystem.
  108. *
  109. * @return array|null
  110. */
  111. protected function loadStoredForm(): ?array
  112. {
  113. $file = $this->getTmpIndex();
  114. $exists = $file && $file->exists();
  115. $data = null;
  116. if ($exists) {
  117. try {
  118. $data = (array)$file->content();
  119. } catch (Exception $e) {
  120. }
  121. }
  122. return $data;
  123. }
  124. /**
  125. * @inheritDoc
  126. */
  127. public function getId(): string
  128. {
  129. return $this->id && $this->uniqueId ? $this->id . '/' . $this->uniqueId : '';
  130. }
  131. /**
  132. * @inheritDoc
  133. */
  134. public function getSessionId(): string
  135. {
  136. return $this->sessionId;
  137. }
  138. /**
  139. * @inheritDoc
  140. */
  141. public function getUniqueId(): string
  142. {
  143. return $this->uniqueId;
  144. }
  145. /**
  146. * @return string
  147. * @deprecated 1.6.11 Use '->getUniqueId()' method instead.
  148. */
  149. public function getUniqieId(): string
  150. {
  151. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED);
  152. return $this->getUniqueId();
  153. }
  154. /**
  155. * @inheritDoc
  156. */
  157. public function getFormName(): string
  158. {
  159. return $this->formName;
  160. }
  161. /**
  162. * @inheritDoc
  163. */
  164. public function getUrl(): string
  165. {
  166. return $this->url;
  167. }
  168. /**
  169. * @inheritDoc
  170. */
  171. public function getUsername(): string
  172. {
  173. return $this->user['username'] ?? '';
  174. }
  175. /**
  176. * @inheritDoc
  177. */
  178. public function getUserEmail(): string
  179. {
  180. return $this->user['email'] ?? '';
  181. }
  182. /**
  183. * @inheritDoc
  184. */
  185. public function getCreatedTimestamp(): int
  186. {
  187. return $this->createdTimestamp;
  188. }
  189. /**
  190. * @inheritDoc
  191. */
  192. public function getUpdatedTimestamp(): int
  193. {
  194. return $this->updatedTimestamp;
  195. }
  196. /**
  197. * @inheritDoc
  198. */
  199. public function getData(): ?array
  200. {
  201. return $this->data;
  202. }
  203. /**
  204. * @inheritDoc
  205. */
  206. public function setData(?array $data): void
  207. {
  208. $this->data = $data;
  209. }
  210. /**
  211. * @inheritDoc
  212. */
  213. public function exists(): bool
  214. {
  215. return $this->exists;
  216. }
  217. /**
  218. * @inheritDoc
  219. */
  220. public function save(bool $force = false)
  221. {
  222. if (!($this->folder && $this->uniqueId)) {
  223. return $this;
  224. }
  225. if ($force || $this->data || $this->files) {
  226. // Only save if there is data or files to be saved.
  227. $file = $this->getTmpIndex();
  228. if ($file) {
  229. $file->save($this->jsonSerialize());
  230. $this->exists = true;
  231. }
  232. } elseif ($this->exists) {
  233. // Delete empty form flash if it exists (it carries no information).
  234. return $this->delete();
  235. }
  236. return $this;
  237. }
  238. /**
  239. * @inheritDoc
  240. */
  241. public function delete()
  242. {
  243. if ($this->folder && $this->uniqueId) {
  244. $this->removeTmpDir();
  245. $this->files = [];
  246. $this->exists = false;
  247. }
  248. return $this;
  249. }
  250. /**
  251. * @inheritDoc
  252. */
  253. public function getFilesByField(string $field): array
  254. {
  255. if (!isset($this->uploadObjects[$field])) {
  256. $objects = [];
  257. foreach ($this->files[$field] ?? [] as $name => $upload) {
  258. $objects[$name] = $upload ? new FormFlashFile($field, $upload, $this) : null;
  259. }
  260. $this->uploadedFiles[$field] = $objects;
  261. }
  262. return $this->uploadedFiles[$field];
  263. }
  264. /**
  265. * @inheritDoc
  266. */
  267. public function getFilesByFields($includeOriginal = false): array
  268. {
  269. $list = [];
  270. foreach ($this->files as $field => $values) {
  271. if (!$includeOriginal && strpos($field, '/')) {
  272. continue;
  273. }
  274. $list[$field] = $this->getFilesByField($field);
  275. }
  276. return $list;
  277. }
  278. /**
  279. * @inheritDoc
  280. */
  281. public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string
  282. {
  283. $tmp_dir = $this->getTmpDir();
  284. $tmp_name = Utils::generateRandomString(12);
  285. $name = $upload->getClientFilename();
  286. if (!$name) {
  287. throw new RuntimeException('Uploaded file has no filename');
  288. }
  289. // Prepare upload data for later save
  290. $data = [
  291. 'name' => $name,
  292. 'type' => $upload->getClientMediaType(),
  293. 'size' => $upload->getSize(),
  294. 'tmp_name' => $tmp_name
  295. ];
  296. Folder::create($tmp_dir);
  297. $upload->moveTo("{$tmp_dir}/{$tmp_name}");
  298. $this->addFileInternal($field, $name, $data, $crop);
  299. return $name;
  300. }
  301. /**
  302. * @inheritDoc
  303. */
  304. public function addFile(string $filename, string $field, array $crop = null): bool
  305. {
  306. if (!file_exists($filename)) {
  307. throw new RuntimeException("File not found: {$filename}");
  308. }
  309. // Prepare upload data for later save
  310. $data = [
  311. 'name' => Utils::basename($filename),
  312. 'type' => Utils::getMimeByLocalFile($filename),
  313. 'size' => filesize($filename),
  314. ];
  315. $this->addFileInternal($field, $data['name'], $data, $crop);
  316. return true;
  317. }
  318. /**
  319. * @inheritDoc
  320. */
  321. public function removeFile(string $name, string $field = null): bool
  322. {
  323. if (!$name) {
  324. return false;
  325. }
  326. $field = $field ?: 'undefined';
  327. $upload = $this->files[$field][$name] ?? null;
  328. if (null !== $upload) {
  329. $this->removeTmpFile($upload['tmp_name'] ?? '');
  330. }
  331. $upload = $this->files[$field . '/original'][$name] ?? null;
  332. if (null !== $upload) {
  333. $this->removeTmpFile($upload['tmp_name'] ?? '');
  334. }
  335. // Mark file as deleted.
  336. $this->files[$field][$name] = null;
  337. $this->files[$field . '/original'][$name] = null;
  338. unset(
  339. $this->uploadedFiles[$field][$name],
  340. $this->uploadedFiles[$field . '/original'][$name]
  341. );
  342. return true;
  343. }
  344. /**
  345. * @inheritDoc
  346. */
  347. public function clearFiles()
  348. {
  349. foreach ($this->files as $files) {
  350. foreach ($files as $upload) {
  351. $this->removeTmpFile($upload['tmp_name'] ?? '');
  352. }
  353. }
  354. $this->files = [];
  355. }
  356. /**
  357. * @inheritDoc
  358. */
  359. public function jsonSerialize(): array
  360. {
  361. return [
  362. 'form' => $this->formName,
  363. 'id' => $this->getId(),
  364. 'unique_id' => $this->uniqueId,
  365. 'url' => $this->url,
  366. 'user' => $this->user,
  367. 'timestamps' => [
  368. 'created' => $this->createdTimestamp,
  369. 'updated' => time(),
  370. ],
  371. 'data' => $this->data,
  372. 'files' => $this->files
  373. ];
  374. }
  375. /**
  376. * @param string $url
  377. * @return $this
  378. */
  379. public function setUrl(string $url): self
  380. {
  381. $this->url = $url;
  382. return $this;
  383. }
  384. /**
  385. * @param UserInterface|null $user
  386. * @return $this
  387. */
  388. public function setUser(UserInterface $user = null)
  389. {
  390. if ($user && $user->username) {
  391. $this->user = [
  392. 'username' => $user->username,
  393. 'email' => $user->email ?? ''
  394. ];
  395. } else {
  396. $this->user = null;
  397. }
  398. return $this;
  399. }
  400. /**
  401. * @param string|null $username
  402. * @return $this
  403. */
  404. public function setUserName(string $username = null): self
  405. {
  406. $this->user['username'] = $username;
  407. return $this;
  408. }
  409. /**
  410. * @param string|null $email
  411. * @return $this
  412. */
  413. public function setUserEmail(string $email = null): self
  414. {
  415. $this->user['email'] = $email;
  416. return $this;
  417. }
  418. /**
  419. * @return string
  420. */
  421. public function getTmpDir(): string
  422. {
  423. return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : '';
  424. }
  425. /**
  426. * @return ?YamlFile
  427. */
  428. protected function getTmpIndex(): ?YamlFile
  429. {
  430. $tmpDir = $this->getTmpDir();
  431. // Do not use CompiledYamlFile as the file can change multiple times per second.
  432. return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
  433. }
  434. /**
  435. * @param string $name
  436. */
  437. protected function removeTmpFile(string $name): void
  438. {
  439. $tmpDir = $this->getTmpDir();
  440. $filename = $tmpDir ? $tmpDir . '/' . $name : '';
  441. if ($name && $filename && is_file($filename)) {
  442. unlink($filename);
  443. }
  444. }
  445. /**
  446. * @return void
  447. */
  448. protected function removeTmpDir(): void
  449. {
  450. // Make sure that index file cache gets always cleared.
  451. $file = $this->getTmpIndex();
  452. if ($file) {
  453. $file->free();
  454. }
  455. $tmpDir = $this->getTmpDir();
  456. if ($tmpDir && file_exists($tmpDir)) {
  457. Folder::delete($tmpDir);
  458. }
  459. }
  460. /**
  461. * @param string|null $field
  462. * @param string $name
  463. * @param array $data
  464. * @param array|null $crop
  465. * @return void
  466. */
  467. protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void
  468. {
  469. if (!($this->folder && $this->uniqueId)) {
  470. throw new RuntimeException('Cannot upload files: form flash folder not defined');
  471. }
  472. $field = $field ?: 'undefined';
  473. if (!isset($this->files[$field])) {
  474. $this->files[$field] = [];
  475. }
  476. $oldUpload = $this->files[$field][$name] ?? null;
  477. if ($crop) {
  478. // Deal with crop upload
  479. if ($oldUpload) {
  480. $originalUpload = $this->files[$field . '/original'][$name] ?? null;
  481. if ($originalUpload) {
  482. // If there is original file already present, remove the modified file
  483. $this->files[$field . '/original'][$name]['crop'] = $crop;
  484. $this->removeTmpFile($oldUpload['tmp_name'] ?? '');
  485. } else {
  486. // Otherwise make the previous file as original
  487. $oldUpload['crop'] = $crop;
  488. $this->files[$field . '/original'][$name] = $oldUpload;
  489. }
  490. } else {
  491. $this->files[$field . '/original'][$name] = [
  492. 'name' => $name,
  493. 'type' => $data['type'],
  494. 'crop' => $crop
  495. ];
  496. }
  497. } else {
  498. // Deal with replacing upload
  499. $originalUpload = $this->files[$field . '/original'][$name] ?? null;
  500. $this->files[$field . '/original'][$name] = null;
  501. $this->removeTmpFile($oldUpload['tmp_name'] ?? '');
  502. $this->removeTmpFile($originalUpload['tmp_name'] ?? '');
  503. }
  504. // Prepare data to be saved later
  505. $this->files[$field][$name] = $data;
  506. }
  507. }