FormFlash.php 13 KB

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