FormTrait.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  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\Traits;
  9. use Grav\Common\Data\Blueprint;
  10. use Grav\Common\Data\Data;
  11. use Grav\Common\Data\ValidationException;
  12. use Grav\Common\Form\FormFlash;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Twig\Twig;
  15. use Grav\Common\User\Interfaces\UserInterface;
  16. use Grav\Common\Utils;
  17. use Grav\Framework\ContentBlock\HtmlBlock;
  18. use Grav\Framework\Form\Interfaces\FormInterface;
  19. use Grav\Framework\Session\SessionInterface;
  20. use Psr\Http\Message\ServerRequestInterface;
  21. use Psr\Http\Message\UploadedFileInterface;
  22. use Twig\Error\LoaderError;
  23. use Twig\Error\SyntaxError;
  24. use Twig\TemplateWrapper;
  25. /**
  26. * Trait FormTrait
  27. * @package Grav\Framework\Form
  28. */
  29. trait FormTrait
  30. {
  31. /** @var string */
  32. public $status = 'success';
  33. /** @var string */
  34. public $message;
  35. /** @var string[] */
  36. public $messages = [];
  37. /** @var string */
  38. private $name;
  39. /** @var string */
  40. private $id;
  41. /** @var string */
  42. private $uniqueid;
  43. /** @var bool */
  44. private $submitted;
  45. /** @var Data|object|null */
  46. private $data;
  47. /** @var array|UploadedFileInterface[] */
  48. private $files;
  49. /** @var FormFlash|null */
  50. private $flash;
  51. /** @var Blueprint */
  52. private $blueprint;
  53. public function getId(): string
  54. {
  55. return $this->id;
  56. }
  57. public function setId(string $id): void
  58. {
  59. $this->id = $id;
  60. }
  61. public function getUniqueId(): string
  62. {
  63. return $this->uniqueid;
  64. }
  65. public function setUniqueId(string $uniqueId): void
  66. {
  67. $this->uniqueid = $uniqueId;
  68. }
  69. public function getName(): string
  70. {
  71. return $this->name;
  72. }
  73. public function getFormName(): string
  74. {
  75. return $this->name;
  76. }
  77. public function getNonceName(): string
  78. {
  79. return 'form-nonce';
  80. }
  81. public function getNonceAction(): string
  82. {
  83. return 'form';
  84. }
  85. public function getNonce(): string
  86. {
  87. return Utils::getNonce($this->getNonceAction());
  88. }
  89. public function getAction(): string
  90. {
  91. return '';
  92. }
  93. public function getTask(): string
  94. {
  95. return $this->getBlueprint()->get('form/task') ?? '';
  96. }
  97. public function getData(string $name = null)
  98. {
  99. return null !== $name ? $this->data[$name] : $this->data;
  100. }
  101. /**
  102. * @return array|UploadedFileInterface[]
  103. */
  104. public function getFiles(): array
  105. {
  106. return $this->files ?? [];
  107. }
  108. public function getValue(string $name)
  109. {
  110. return $this->data[$name] ?? null;
  111. }
  112. public function getDefaultValue(string $name)
  113. {
  114. $path = explode('.', $name) ?: [];
  115. $offset = array_shift($path) ?? '';
  116. $current = $this->getDefaultValues();
  117. if (!isset($current[$offset])) {
  118. return null;
  119. }
  120. $current = $current[$offset];
  121. while ($path) {
  122. $offset = array_shift($path);
  123. if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
  124. $current = $current[$offset];
  125. } elseif (\is_object($current) && isset($current->{$offset})) {
  126. $current = $current->{$offset};
  127. } else {
  128. return null;
  129. }
  130. };
  131. return $current;
  132. }
  133. /**
  134. * @return array
  135. */
  136. public function getDefaultValues(): array
  137. {
  138. return $this->getBlueprint()->getDefaults();
  139. }
  140. /**
  141. * @param ServerRequestInterface $request
  142. * @return FormInterface|$this
  143. */
  144. public function handleRequest(ServerRequestInterface $request): FormInterface
  145. {
  146. // Set current form to be active.
  147. $grav = Grav::instance();
  148. $forms = $grav['forms'] ?? null;
  149. if ($forms) {
  150. $forms->setActiveForm($this);
  151. /** @var Twig $twig */
  152. $twig = $grav['twig'];
  153. $twig->twig_vars['form'] = $this;
  154. }
  155. try {
  156. [$data, $files] = $this->parseRequest($request);
  157. $this->submit($data, $files);
  158. } catch (\Exception $e) {
  159. $this->setError($e->getMessage());
  160. }
  161. return $this;
  162. }
  163. /**
  164. * @param ServerRequestInterface $request
  165. * @return FormInterface|$this
  166. */
  167. public function setRequest(ServerRequestInterface $request): FormInterface
  168. {
  169. [$data, $files] = $this->parseRequest($request);
  170. $this->data = new Data($data, $this->getBlueprint());
  171. $this->files = $files;
  172. return $this;
  173. }
  174. public function isValid(): bool
  175. {
  176. return $this->status === 'success';
  177. }
  178. public function getError(): ?string
  179. {
  180. return !$this->isValid() ? $this->message : null;
  181. }
  182. public function getErrors(): array
  183. {
  184. return !$this->isValid() ? $this->messages : [];
  185. }
  186. public function isSubmitted(): bool
  187. {
  188. return $this->submitted;
  189. }
  190. public function validate(): bool
  191. {
  192. if (!$this->isValid()) {
  193. return false;
  194. }
  195. try {
  196. $this->validateData($this->data);
  197. $this->validateUploads($this->getFiles());
  198. } catch (ValidationException $e) {
  199. $this->setErrors($e->getMessages());
  200. } catch (\Exception $e) {
  201. $this->setError($e->getMessage());
  202. }
  203. $this->filterData($this->data);
  204. return $this->isValid();
  205. }
  206. /**
  207. * @param array $data
  208. * @param UploadedFileInterface[] $files
  209. * @return FormInterface|$this
  210. */
  211. public function submit(array $data, array $files = null): FormInterface
  212. {
  213. try {
  214. if ($this->isSubmitted()) {
  215. throw new \RuntimeException('Form has already been submitted');
  216. }
  217. $this->data = new Data($data, $this->getBlueprint());
  218. $this->files = $files ?? [];
  219. if (!$this->validate()) {
  220. return $this;
  221. }
  222. $this->doSubmit($this->data->toArray(), $this->files);
  223. $this->submitted = true;
  224. } catch (\Exception $e) {
  225. $this->setError($e->getMessage());
  226. }
  227. return $this;
  228. }
  229. public function reset(): void
  230. {
  231. // Make sure that the flash object gets deleted.
  232. $this->getFlash()->delete();
  233. $this->data = null;
  234. $this->files = [];
  235. $this->status = 'success';
  236. $this->message = null;
  237. $this->messages = [];
  238. $this->submitted = false;
  239. $this->flash = null;
  240. }
  241. public function getFields(): array
  242. {
  243. return $this->getBlueprint()->fields();
  244. }
  245. public function getButtons(): array
  246. {
  247. return $this->getBlueprint()->get('form/buttons') ?? [];
  248. }
  249. public function getTasks(): array
  250. {
  251. return $this->getBlueprint()->get('form/tasks') ?? [];
  252. }
  253. abstract public function getBlueprint(): Blueprint;
  254. /**
  255. * Implements \Serializable::serialize().
  256. *
  257. * @return string
  258. */
  259. public function serialize(): string
  260. {
  261. return serialize($this->doSerialize());
  262. }
  263. /**
  264. * Implements \Serializable::unserialize().
  265. *
  266. * @param string $serialized
  267. */
  268. public function unserialize($serialized): void
  269. {
  270. $data = unserialize($serialized, ['allowed_classes' => false]);
  271. $this->doUnserialize($data);
  272. }
  273. /**
  274. * Get form flash object.
  275. *
  276. * @return FormFlash
  277. */
  278. public function getFlash(): FormFlash
  279. {
  280. if (null === $this->flash) {
  281. $grav = Grav::instance();
  282. $config = [
  283. 'session_id' => $this->getSessionId(),
  284. 'unique_id' => $this->getUniqueId(),
  285. 'form_name' => $this->getName(),
  286. 'folder' => $this->getFlashFolder()
  287. ];
  288. $this->flash = new FormFlash($config);
  289. $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
  290. }
  291. return $this->flash;
  292. }
  293. /**
  294. * Get all available form flash objects for this form.
  295. *
  296. * @return FormFlash[]
  297. */
  298. public function getAllFlashes(): array
  299. {
  300. $folder = $this->getFlashFolder();
  301. if (!$folder || !is_dir($folder)) {
  302. return [];
  303. }
  304. $name = $this->getName();
  305. $list = [];
  306. /** @var \SplFileInfo $file */
  307. foreach (new \FilesystemIterator($folder) as $file) {
  308. $uniqueId = $file->getFilename();
  309. $config = [
  310. 'session_id' => $this->getSessionId(),
  311. 'unique_id' => $uniqueId,
  312. 'form_name' => $name,
  313. 'folder' => $this->getFlashFolder()
  314. ];
  315. $flash = new FormFlash($config);
  316. if ($flash->exists() && $flash->getFormName() === $name) {
  317. $list[] = $flash;
  318. }
  319. }
  320. return $list;
  321. }
  322. /**
  323. * {@inheritdoc}
  324. * @see FormInterface::render()
  325. */
  326. public function render(string $layout = null, array $context = [])
  327. {
  328. if (null === $layout) {
  329. $layout = 'default';
  330. }
  331. $grav = Grav::instance();
  332. $block = HtmlBlock::create();
  333. $block->disableCache();
  334. $output = $this->getTemplate($layout)->render(
  335. ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context
  336. );
  337. $block->setContent($output);
  338. return $block;
  339. }
  340. protected function getSessionId(): string
  341. {
  342. /** @var Grav $grav */
  343. $grav = Grav::instance();
  344. /** @var SessionInterface $session */
  345. $session = $grav['session'] ?? null;
  346. return $session ? ($session->getId() ?? '') : '';
  347. }
  348. protected function unsetFlash(): void
  349. {
  350. $this->flash = null;
  351. }
  352. protected function getFlashFolder(): ?string
  353. {
  354. $grav = Grav::instance();
  355. /** @var UserInterface $user */
  356. $user = $grav['user'] ?? null;
  357. $userExists = $user && $user->exists();
  358. $username = $userExists ? $user->username : null;
  359. $mediaFolder = $userExists ? $user->getMediaFolder() : null;
  360. $session = $grav['session'] ?? null;
  361. $sessionId = $session ? $session->getId() : null;
  362. // Fill template token keys/value pairs.
  363. $dataMap = [
  364. '[FORM_NAME]' => $this->getName(),
  365. '[SESSIONID]' => $sessionId ?? '!!',
  366. '[USERNAME]' => $username ?? '!!',
  367. '[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
  368. '[ACCOUNT]' => $mediaFolder ?? '!!'
  369. ];
  370. $flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]');
  371. $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder);
  372. // Make sure we only return valid paths.
  373. return strpos($path, '!!') === false ? rtrim($path, '/') : null;
  374. }
  375. /**
  376. * Set a single error.
  377. *
  378. * @param string $error
  379. */
  380. protected function setError(string $error): void
  381. {
  382. $this->status = 'error';
  383. $this->message = $error;
  384. }
  385. /**
  386. * Set all errors.
  387. *
  388. * @param array $errors
  389. */
  390. protected function setErrors(array $errors): void
  391. {
  392. $this->status = 'error';
  393. $this->messages = $errors;
  394. }
  395. /**
  396. * @param string $layout
  397. * @return TemplateWrapper
  398. * @throws LoaderError
  399. * @throws SyntaxError
  400. */
  401. protected function getTemplate($layout)
  402. {
  403. $grav = Grav::instance();
  404. /** @var Twig $twig */
  405. $twig = $grav['twig'];
  406. return $twig->twig()->resolveTemplate(
  407. [
  408. "forms/{$layout}/form.html.twig",
  409. 'forms/default/form.html.twig'
  410. ]
  411. );
  412. }
  413. /**
  414. * Parse PSR-7 ServerRequest into data and files.
  415. *
  416. * @param ServerRequestInterface $request
  417. * @return array
  418. */
  419. protected function parseRequest(ServerRequestInterface $request): array
  420. {
  421. $method = $request->getMethod();
  422. if (!\in_array($method, ['PUT', 'POST', 'PATCH'])) {
  423. throw new \RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
  424. }
  425. $body = $request->getParsedBody();
  426. $data = isset($body['data']) ? $this->decodeData($body['data']) : null;
  427. $flash = $this->getFlash();
  428. /*
  429. if (null !== $data) {
  430. $flash->setData($data);
  431. $flash->save();
  432. }
  433. */
  434. $blueprint = $this->getBlueprint();
  435. $includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null);
  436. $files = $flash->getFilesByFields($includeOriginal);
  437. $data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []);
  438. return [
  439. $data,
  440. $files ?? []
  441. ];
  442. }
  443. /**
  444. * Form submit logic goes here.
  445. *
  446. * @param array $data
  447. * @param array $files
  448. * @return mixed
  449. */
  450. abstract protected function doSubmit(array $data, array $files);
  451. /**
  452. * Validate data and throw validation exceptions if validation fails.
  453. *
  454. * @param \ArrayAccess $data
  455. * @throws ValidationException
  456. * @throws \Exception
  457. */
  458. protected function validateData(\ArrayAccess $data): void
  459. {
  460. if ($data instanceof Data) {
  461. $data->validate();
  462. }
  463. }
  464. /**
  465. * Filter validated data.
  466. *
  467. * @param \ArrayAccess $data
  468. */
  469. protected function filterData(\ArrayAccess $data): void
  470. {
  471. if ($data instanceof Data) {
  472. $data->filter();
  473. }
  474. }
  475. /**
  476. * Validate all uploaded files.
  477. *
  478. * @param array $files
  479. */
  480. protected function validateUploads(array $files): void
  481. {
  482. foreach ($files as $file) {
  483. if (null === $file) {
  484. continue;
  485. }
  486. if ($file instanceof UploadedFileInterface) {
  487. $this->validateUpload($file);
  488. } else {
  489. $this->validateUploads($file);
  490. }
  491. }
  492. }
  493. /**
  494. * Validate uploaded file.
  495. *
  496. * @param UploadedFileInterface $file
  497. */
  498. protected function validateUpload(UploadedFileInterface $file): void
  499. {
  500. // Handle bad filenames.
  501. $filename = $file->getClientFilename();
  502. if (!Utils::checkFilename($filename)) {
  503. $grav = Grav::instance();
  504. throw new \RuntimeException(
  505. sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
  506. );
  507. }
  508. }
  509. /**
  510. * Decode POST data
  511. *
  512. * @param array $data
  513. * @return array
  514. */
  515. protected function decodeData($data): array
  516. {
  517. if (!\is_array($data)) {
  518. return [];
  519. }
  520. // Decode JSON encoded fields and merge them to data.
  521. if (isset($data['_json'])) {
  522. $data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
  523. unset($data['_json']);
  524. }
  525. return $data;
  526. }
  527. /**
  528. * Recursively JSON decode POST data.
  529. *
  530. * @param array $data
  531. * @return array
  532. */
  533. protected function jsonDecode(array $data): array
  534. {
  535. foreach ($data as $key => &$value) {
  536. if (\is_array($value)) {
  537. $value = $this->jsonDecode($value);
  538. } elseif (trim($value) === '') {
  539. unset($data[$key]);
  540. } else {
  541. $value = json_decode($value, true);
  542. if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
  543. unset($data[$key]);
  544. $this->setError("Badly encoded JSON data (for {$key}) was sent to the form");
  545. }
  546. }
  547. }
  548. return $data;
  549. }
  550. /**
  551. * @return array
  552. */
  553. protected function doSerialize(): array
  554. {
  555. $data = $this->data instanceof Data ? $this->data->toArray() : null;
  556. return [
  557. 'name' => $this->name,
  558. 'id' => $this->id,
  559. 'uniqueid' => $this->uniqueid,
  560. 'submitted' => $this->submitted,
  561. 'status' => $this->status,
  562. 'message' => $this->message,
  563. 'messages' => $this->messages,
  564. 'data' => $data,
  565. 'files' => $this->files,
  566. ];
  567. }
  568. /**
  569. * @param array $data
  570. */
  571. protected function doUnserialize(array $data): void
  572. {
  573. $this->name = $data['name'];
  574. $this->id = $data['id'];
  575. $this->uniqueid = $data['uniqueid'];
  576. $this->submitted = $data['submitted'] ?? false;
  577. $this->status = $data['status'] ?? 'success';
  578. $this->message = $data['message'] ?? null;
  579. $this->messages = $data['messages'] ?? [];
  580. $this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null;
  581. $this->files = $data['files'] ?? [];
  582. }
  583. }