Form.php 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  1. <?php
  2. namespace Grav\Plugin\Form;
  3. use Grav\Common\Config\Config;
  4. use Grav\Common\Data\Data;
  5. use Grav\Common\Data\Blueprint;
  6. use Grav\Common\Data\ValidationException;
  7. use Grav\Common\Filesystem\Folder;
  8. use Grav\Common\Form\FormFlash;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Inflector;
  11. use Grav\Common\Language\Language;
  12. use Grav\Common\Page\Interfaces\PageInterface;
  13. use Grav\Common\Uri;
  14. use Grav\Common\Utils;
  15. use Grav\Framework\Filesystem\Filesystem;
  16. use Grav\Framework\Form\FormFlashFile;
  17. use Grav\Framework\Form\Interfaces\FormInterface;
  18. use Grav\Framework\Form\Traits\FormTrait;
  19. use Grav\Framework\Route\Route;
  20. use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
  21. use RocketTheme\Toolbox\Event\Event;
  22. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  23. /**
  24. * Class Form
  25. * @package Grav\Plugin\Form
  26. *
  27. * @property string $id
  28. * @property string $uniqueid
  29. * @property-read string $name
  30. * @property-read string $noncename
  31. * @property-read $string nonceaction
  32. * @property-read string $action
  33. * @property-read Data $data
  34. * @property-read array $files
  35. * @property-read Data $value
  36. * @property-read array $errors
  37. * @property-read array $fields
  38. * @property-read Blueprint $blueprint
  39. * @property-read PageInterface $page
  40. */
  41. class Form implements FormInterface, \ArrayAccess
  42. {
  43. use NestedArrayAccessWithGetters {
  44. NestedArrayAccessWithGetters::get as private traitGet;
  45. NestedArrayAccessWithGetters::set as private traitSet;
  46. }
  47. use FormTrait {
  48. FormTrait::reset as private traitReset;
  49. FormTrait::doSerialize as private doTraitSerialize;
  50. FormTrait::doUnserialize as private doTraitUnserialize;
  51. }
  52. public const BYTES_TO_MB = 1048576;
  53. /**
  54. * @var string
  55. */
  56. public $message;
  57. /**
  58. * @var int
  59. */
  60. public $response_code;
  61. /**
  62. * @var string
  63. */
  64. public $status = 'success';
  65. /**
  66. * @var array
  67. */
  68. protected $header_data = [];
  69. /**
  70. * @var array
  71. */
  72. protected $rules = [];
  73. /**
  74. * Form header items
  75. *
  76. * @var array $items
  77. */
  78. protected $items = [];
  79. /**
  80. * All the form data values, including non-data
  81. *
  82. * @var Data $values
  83. */
  84. protected $values;
  85. /**
  86. * The form page route
  87. *
  88. * @var string $page
  89. */
  90. protected $page;
  91. /**
  92. * Create form for the given page.
  93. *
  94. * @param PageInterface $page
  95. * @param string|int|null $name
  96. * @param array|null $form
  97. */
  98. public function __construct(PageInterface $page, $name = null, $form = null)
  99. {
  100. $this->nestedSeparator = '/';
  101. $slug = $page->slug();
  102. $header = $page->header();
  103. $this->rules = $header->rules ?? [];
  104. $this->header_data = $header->data ?? [];
  105. if ($form) {
  106. // If form is given, use it.
  107. $this->items = $form;
  108. } else {
  109. // Otherwise get all forms in the page.
  110. $forms = $page->forms();
  111. if ($name) {
  112. // If form with given name was found, use that.
  113. $this->items = $forms[$name] ?? [];
  114. } else {
  115. // Otherwise pick up the first form.
  116. $this->items = reset($forms) ?: [];
  117. $name = key($forms);
  118. }
  119. }
  120. // If we're on a modular page, find the real page.
  121. while ($page && $page->modular()) {
  122. $header = $page->header();
  123. $header->never_cache_twig = true;
  124. $page = $page->parent();
  125. }
  126. $this->page = $page ? $page->route() : '/';
  127. // Add form specific rules.
  128. if (!empty($this->items['rules']) && \is_array($this->items['rules'])) {
  129. $this->rules += $this->items['rules'];
  130. }
  131. // Set form name if not set.
  132. if ($name && !\is_int($name)) {
  133. $this->items['name'] = $name;
  134. } elseif (empty($this->items['name'])) {
  135. $this->items['name'] = $slug;
  136. }
  137. // Set form id if not set.
  138. if (empty($this->items['id'])) {
  139. $this->items['id'] = Inflector::hyphenize($this->items['name']);
  140. }
  141. if (empty($this->items['nonce']['name'])) {
  142. $this->items['nonce']['name'] = 'form-nonce';
  143. }
  144. if (empty($this->items['nonce']['action'])) {
  145. $this->items['nonce']['action'] = 'form';
  146. }
  147. // Initialize form properties.
  148. $this->name = $this->items['name'];
  149. $this->setId($this->items['id']);
  150. $uniqueid = $this->items['uniqueid'] ?? null;
  151. if (null === $uniqueid && !empty($this->items['remember_state'])) {
  152. $this->set('remember_redirect', true);
  153. }
  154. $this->setUniqueId($uniqueid ?? strtolower(Utils::generateRandomString($this->items['uniqueid_len'] ?? 20)));
  155. $this->initialize();
  156. }
  157. /**
  158. * @return $this
  159. */
  160. public function initialize()
  161. {
  162. // Reset and initialize the form
  163. $this->errors = [];
  164. $this->submitted = false;
  165. $this->unsetFlash();
  166. // Remember form state.
  167. $flash = $this->getFlash();
  168. if ($flash->exists()) {
  169. $data = $flash->getData() ?? $this->header_data;
  170. } else {
  171. $data = $this->header_data;
  172. }
  173. // Remember data and files.
  174. $this->setAllData($data);
  175. $this->setAllFiles($flash);
  176. $this->values = new Data();
  177. // Fire event
  178. $grav = Grav::instance();
  179. $grav->fireEvent('onFormInitialized', new Event(['form' => $this]));
  180. return $this;
  181. }
  182. protected function setAllFiles(FormFlash $flash)
  183. {
  184. if (!$flash->exists()) {
  185. return;
  186. }
  187. /** @var Uri $url */
  188. $url = Grav::instance()['uri'];
  189. $fields = $flash->getFilesByFields(true);
  190. foreach ($fields as $field => $files) {
  191. if (strpos($field, '/') !== false) {
  192. continue;
  193. }
  194. $list = [];
  195. /**
  196. * @var string $filename
  197. * @var FormFlashFile $file
  198. */
  199. foreach ($files as $filename => $file) {
  200. $original = $fields["{$field}/original"][$filename] ?? $file;
  201. $basename = basename($filename);
  202. if ($file) {
  203. $imagePath = $original->getTmpFile();
  204. $thumbPath = $file->getTmpFile();
  205. $list[$basename] = [
  206. 'name' => $file->getClientFilename(),
  207. 'type' => $file->getClientMediaType(),
  208. 'size' => $file->getSize(),
  209. 'image_url' => $url->rootUrl() . '/' . Folder::getRelativePath($imagePath) . '?' . filemtime($imagePath),
  210. 'thumb_url' => $url->rootUrl() . '/' . Folder::getRelativePath($thumbPath) . '?' . filemtime($thumbPath),
  211. 'cropData' => $original->getMetaData()['crop'] ?? []
  212. ];
  213. }
  214. }
  215. $this->setData($field, $list);
  216. }
  217. }
  218. /**
  219. * Reset form.
  220. */
  221. public function reset(): void
  222. {
  223. $this->traitReset();
  224. // Reset and initialize the form
  225. $this->blueprint = null;
  226. $this->setAllData($this->header_data);
  227. $this->values = new Data();
  228. // Reset unique id (allow multiple form submits)
  229. $uniqueid = $this->items['uniqueid'] ?? null;
  230. $this->set('remember_redirect', null === $uniqueid && !empty($this->items['remember_state']));
  231. $this->setUniqueId($uniqueid ?? strtolower(Utils::generateRandomString($this->items['uniqueid_len'] ?? 20)));
  232. // Fire event
  233. $grav = Grav::instance();
  234. $grav->fireEvent('onFormInitialized', new Event(['form' => $this]));
  235. }
  236. public function get($name, $default = null, $separator = null)
  237. {
  238. switch (strtolower($name)) {
  239. case 'id':
  240. case 'uniqueid':
  241. case 'name':
  242. case 'noncename':
  243. case 'nonceaction':
  244. case 'action':
  245. case 'data':
  246. case 'files':
  247. case 'errors';
  248. case 'fields':
  249. case 'blueprint':
  250. case 'page':
  251. $method = 'get' . $name;
  252. return $this->{$method}();
  253. }
  254. return $this->traitGet($name, $default, $separator);
  255. }
  256. public function getAction(): string
  257. {
  258. return $this->items['action'] ?? $this->page;
  259. }
  260. /**
  261. * @param $message
  262. * @param string $type
  263. * @todo Type not used
  264. */
  265. public function setMessage($message, $type = 'error')
  266. {
  267. $this->setError($message);
  268. }
  269. public function set($name, $value, $separator = null)
  270. {
  271. switch (strtolower($name)) {
  272. case 'id':
  273. case 'uniqueid':
  274. $method = 'set' . $name;
  275. return $this->{$method}();
  276. }
  277. return $this->traitSet($name, $value, $separator);
  278. }
  279. /**
  280. * Get the nonce value for a form
  281. *
  282. * @return string
  283. */
  284. public function getNonce(): string
  285. {
  286. return Utils::getNonce($this->getNonceAction());
  287. }
  288. /**
  289. * @inheritdoc
  290. */
  291. public function getNonceName(): string
  292. {
  293. return $this->items['nonce']['name'];
  294. }
  295. /**
  296. * @inheritdoc
  297. */
  298. public function getNonceAction(): string
  299. {
  300. return $this->items['nonce']['action'];
  301. }
  302. /**
  303. * @inheritdoc
  304. */
  305. public function getValue(string $name)
  306. {
  307. return $this->values->get($name);
  308. }
  309. /**
  310. * @return Data
  311. */
  312. public function getValues(): Data
  313. {
  314. return $this->values;
  315. }
  316. /**
  317. * @inheritdoc
  318. */
  319. public function getFields(): array
  320. {
  321. return $this->getBlueprint()->fields();
  322. }
  323. /**
  324. * Return page object for the form.
  325. *
  326. * @return PageInterface
  327. */
  328. public function getPage(): PageInterface
  329. {
  330. return Grav::instance()['pages']->dispatch($this->page);
  331. }
  332. /**
  333. * @inheritdoc
  334. */
  335. public function getBlueprint(): Blueprint
  336. {
  337. if (null === $this->blueprint) {
  338. // Fix naming for fields (supports nested fields now!)
  339. if (isset($this->items['fields'])) {
  340. $this->items['fields'] = $this->processFields($this->items['fields']);
  341. }
  342. $blueprint = new Blueprint($this->name, ['form' => $this->items, 'rules' => $this->rules]);
  343. $blueprint->load()->init();
  344. $this->blueprint = $blueprint;
  345. }
  346. return $this->blueprint;
  347. }
  348. /**
  349. * Allow overriding of fields.
  350. *
  351. * @param array $fields
  352. */
  353. public function setFields(array $fields = [])
  354. {
  355. $this->items['fields'] = $fields;
  356. unset($this->items['field']);
  357. // Reset blueprint.
  358. $this->blueprint = null;
  359. // Update data to contain the new blueprints.
  360. $this->setAllData($this->data->toArray());
  361. }
  362. /**
  363. * Get value of given variable (or all values).
  364. * First look in the $data array, fallback to the $values array
  365. *
  366. * @param string $name
  367. * @param bool $fallback
  368. * @return mixed
  369. */
  370. public function value($name = null, $fallback = false)
  371. {
  372. if (!$name) {
  373. return $this->data;
  374. }
  375. if (isset($this->data[$name])) {
  376. return $this->data[$name];
  377. }
  378. if ($fallback) {
  379. return $this->values[$name];
  380. }
  381. return null;
  382. }
  383. /**
  384. * Get value of given variable (or all values).
  385. *
  386. * @param string $name
  387. * @return mixed
  388. */
  389. public function data($name = null)
  390. {
  391. return $this->value($name);
  392. }
  393. /**
  394. * Set value of given variable in the values array
  395. *
  396. * @param string $name
  397. * @param mixed $value
  398. */
  399. public function setValue($name = null, $value = '')
  400. {
  401. if (!$name) {
  402. return;
  403. }
  404. $this->values->set($name, $value);
  405. }
  406. /**
  407. * Set value of given variable in the data array
  408. *
  409. * @param string $name
  410. * @param string $value
  411. *
  412. * @return bool
  413. */
  414. public function setData($name = null, $value = '')
  415. {
  416. if (!$name) {
  417. return false;
  418. }
  419. $this->data->set($name, $value);
  420. return true;
  421. }
  422. public function setAllData($array): void
  423. {
  424. $callable = function () {
  425. return $this->getBlueprint();
  426. };
  427. $this->data = new Data($array, $callable);
  428. }
  429. /**
  430. * Handles ajax upload for files.
  431. * Stores in a flash object the temporary file and deals with potential file errors.
  432. *
  433. * @return mixed True if the action was performed.
  434. */
  435. public function uploadFiles()
  436. {
  437. $grav = Grav::instance();
  438. /** @var Uri $uri */
  439. $uri = $grav['uri'];
  440. $url = $uri->url;
  441. $post = $uri->post();
  442. $name = $post['name'] ?? null;
  443. $task = $post['task'] ?? null;
  444. /** @var Language $language */
  445. $language = $grav['language'];
  446. /** @var Config $config */
  447. $config = $grav['config'];
  448. $settings = $this->getBlueprint()->schema()->getProperty($name);
  449. $settings = (object) array_merge(
  450. ['destination' => $config->get('plugins.form.files.destination', 'self@'),
  451. 'avoid_overwriting' => $config->get('plugins.form.files.avoid_overwriting', false),
  452. 'random_name' => $config->get('plugins.form.files.random_name', false),
  453. 'accept' => $config->get('plugins.form.files.accept', ['image/*']),
  454. 'limit' => $config->get('plugins.form.files.limit', 10),
  455. 'filesize' => static::getMaxFilesize(),
  456. ],
  457. (array) $settings,
  458. ['name' => $name]
  459. );
  460. // Allow plugins to adapt settings for a given post name
  461. // Useful if schema retrieval is not an option, e.g. dynamically created forms
  462. $grav->fireEvent('onFormUploadSettings', new Event(['settings' => &$settings, 'post' => $post]));
  463. $upload = json_decode(json_encode($this->normalizeFiles($_FILES['data'], $settings->name)), true);
  464. $filename = $post['filename'] ?? $upload['file']['name'];
  465. $field = $upload['field'];
  466. // Handle errors and breaks without proceeding further
  467. if ($upload['file']['error'] !== UPLOAD_ERR_OK) {
  468. // json_response
  469. return [
  470. 'status' => 'error',
  471. 'message' => sprintf($language->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, $this->upload_errors[$upload['file']['error']])
  472. ];
  473. }
  474. // Handle bad filenames.
  475. if (!Utils::checkFilename($filename)) {
  476. return [
  477. 'status' => 'error',
  478. 'message' => sprintf($language->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
  479. $filename, 'Bad filename')
  480. ];
  481. }
  482. if (!isset($settings->destination)) {
  483. return [
  484. 'status' => 'error',
  485. 'message' => $language->translate('PLUGIN_FORM.DESTINATION_NOT_SPECIFIED', null)
  486. ];
  487. }
  488. // Remove the error object to avoid storing it
  489. unset($upload['file']['error']);
  490. // Handle Accepted file types
  491. // Accept can only be mime types (image/png | image/*) or file extensions (.pdf|.jpg)
  492. $accepted = false;
  493. $errors = [];
  494. // Do not trust mimetype sent by the browser
  495. $mime = Utils::getMimeByFilename($filename);
  496. foreach ((array)$settings->accept as $type) {
  497. // Force acceptance of any file when star notation
  498. if ($type === '*') {
  499. $accepted = true;
  500. break;
  501. }
  502. $isMime = strstr($type, '/');
  503. $find = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
  504. if ($isMime) {
  505. $match = preg_match('#' . $find . '$#', $mime);
  506. if (!$match) {
  507. $errors[] = sprintf($language->translate('PLUGIN_FORM.INVALID_MIME_TYPE', null, true), $mime, $filename);
  508. } else {
  509. $accepted = true;
  510. break;
  511. }
  512. } else {
  513. $match = preg_match('#' . $find . '$#', $filename);
  514. if (!$match) {
  515. $errors[] = sprintf($language->translate('PLUGIN_FORM.INVALID_FILE_EXTENSION', null, true), $filename);
  516. } else {
  517. $accepted = true;
  518. break;
  519. }
  520. }
  521. }
  522. if (!$accepted) {
  523. // json_response
  524. return [
  525. 'status' => 'error',
  526. 'message' => implode('<br/>', $errors)
  527. ];
  528. }
  529. // Handle file size limits
  530. $settings->filesize *= self::BYTES_TO_MB; // 1024 * 1024 [MB in Bytes]
  531. if ($settings->filesize > 0 && $upload['file']['size'] > $settings->filesize) {
  532. // json_response
  533. return [
  534. 'status' => 'error',
  535. 'message' => $language->translate('PLUGIN_FORM.EXCEEDED_GRAV_FILESIZE_LIMIT')
  536. ];
  537. }
  538. // Generate random name if required
  539. if ($settings->random_name) {
  540. $extension = pathinfo($filename, PATHINFO_EXTENSION);
  541. $filename = Utils::generateRandomString(15) . '.' . $extension;
  542. }
  543. // Look up for destination
  544. /** @var UniformResourceLocator $locator */
  545. $locator = $grav['locator'];
  546. $destination = $settings->destination;
  547. if (!$locator->isStream($destination)) {
  548. $destination = $this->getPagePathFromToken(Folder::getRelativePath(rtrim($settings->destination, '/')));
  549. }
  550. // Handle conflicting name if needed
  551. if ($settings->avoid_overwriting) {
  552. if (file_exists($destination . '/' . $filename)) {
  553. $filename = date('YmdHis') . '-' . $filename;
  554. }
  555. }
  556. // Prepare object for later save
  557. $path = $destination . '/' . $filename;
  558. $upload['file']['name'] = $filename;
  559. $upload['file']['path'] = $path;
  560. // We need to store the file into flash object or it will not be available upon save later on.
  561. $flash = $this->getFlash();
  562. $flash->setUrl($url)->setUser($grav['user'] ?? null);
  563. if ($task === 'cropupload') {
  564. $crop = $post['crop'];
  565. if (\is_string($crop)) {
  566. $crop = json_decode($crop, true);
  567. }
  568. $success = $flash->cropFile($field, $filename, $upload, $crop);
  569. } else {
  570. $success = $flash->uploadFile($field, $filename, $upload);
  571. }
  572. if (!$success) {
  573. // json_response
  574. return [
  575. 'status' => 'error',
  576. 'message' => sprintf($language->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '', $flash->getTmpDir())
  577. ];
  578. }
  579. $flash->save();
  580. // json_response
  581. $json_response = [
  582. 'status' => 'success',
  583. 'session' => \json_encode([
  584. 'sessionField' => base64_encode($url),
  585. 'path' => $path,
  586. 'field' => $settings->name,
  587. 'uniqueid' => $this->uniqueid
  588. ])
  589. ];
  590. // Return JSON
  591. header('Content-Type: application/json');
  592. echo json_encode($json_response);
  593. exit;
  594. }
  595. /**
  596. * Removes a file from the flash object session, before it gets saved.
  597. */
  598. public function filesSessionRemove(): void
  599. {
  600. $callable = function (): array {
  601. $field = $this->values->get('name');
  602. $filename = $this->values->get('filename');
  603. if (!isset($field, $filename)) {
  604. throw new \RuntimeException('Bad Request: name and/or filename are missing', 400);
  605. }
  606. $this->removeFlashUpload($filename, $field);
  607. return ['status' => 'success'];
  608. };
  609. $this->sendJsonResponse($callable);
  610. }
  611. public function storeState(): void
  612. {
  613. $callable = function (): array {
  614. $this->updateFlashData($this->values->get('data') ?? []);
  615. return ['status' => 'success'];
  616. };
  617. $this->sendJsonResponse($callable);
  618. }
  619. public function clearState(): void
  620. {
  621. $callable = function (): array {
  622. $this->getFlash()->delete();
  623. return ['status' => 'success'];
  624. };
  625. $this->sendJsonResponse($callable);
  626. }
  627. /**
  628. * Handle form processing on POST action.
  629. */
  630. public function post()
  631. {
  632. $grav = Grav::instance();
  633. /** @var Uri $uri */
  634. $uri = $grav['uri'];
  635. // Get POST data and decode JSON fields into arrays
  636. $post = $uri->post();
  637. $post['data'] = $this->decodeData($post['data'] ?? []);
  638. if ($post) {
  639. $this->values = new Data((array)$post);
  640. $data = $this->values->get('data');
  641. // Add post data to form dataset
  642. if (!$data) {
  643. $data = $this->values->toArray();
  644. }
  645. if (!$this->values->get('form-nonce') || !Utils::verifyNonce($this->values->get('form-nonce'), 'form')) {
  646. $this->status = 'error';
  647. $event = new Event(['form' => $this,
  648. 'message' => $grav['language']->translate('PLUGIN_FORM.NONCE_NOT_VALIDATED')
  649. ]);
  650. $grav->fireEvent('onFormValidationError', $event);
  651. return;
  652. }
  653. $i = 0;
  654. foreach ($this->items['fields'] as $key => $field) {
  655. $name = $field['name'] ?? $key;
  656. if (!isset($field['name'])) {
  657. if (isset($data[$i])) { //Handle input@ false fields
  658. $data[$name] = $data[$i];
  659. unset($data[$i]);
  660. }
  661. }
  662. if ($field['type'] === 'checkbox' || $field['type'] === 'switch') {
  663. $data[$name] = isset($data[$name]) ? true : false;
  664. }
  665. $i++;
  666. }
  667. $this->data->merge($data);
  668. }
  669. // Validate and filter data
  670. try {
  671. $grav->fireEvent('onFormPrepareValidation', new Event(['form' => $this]));
  672. $this->data->validate();
  673. $this->data->filter();
  674. $grav->fireEvent('onFormValidationProcessed', new Event(['form' => $this]));
  675. } catch (ValidationException $e) {
  676. $this->status = 'error';
  677. $event = new Event(['form' => $this, 'message' => $e->getMessage(), 'messages' => $e->getMessages()]);
  678. $grav->fireEvent('onFormValidationError', $event);
  679. if ($event->isPropagationStopped()) {
  680. return;
  681. }
  682. } catch (\RuntimeException $e) {
  683. $this->status = 'error';
  684. $event = new Event(['form' => $this, 'message' => $e->getMessage(), 'messages' => []]);
  685. $grav->fireEvent('onFormValidationError', $event);
  686. if ($event->isPropagationStopped()) {
  687. return;
  688. }
  689. }
  690. $redirect = $redirect_code = null;
  691. $process = $this->items['process'] ?? [];
  692. $legacyUploads = !isset($process['upload']) || $process['upload'] !== false;
  693. if ($legacyUploads) {
  694. $this->legacyUploads();
  695. }
  696. if (\is_array($process)) {
  697. foreach ($process as $action => $data) {
  698. if (is_numeric($action)) {
  699. $action = \key($data);
  700. $data = $data[$action];
  701. }
  702. $event = new Event(['form' => $this, 'action' => $action, 'params' => $data]);
  703. $grav->fireEvent('onFormProcessed', $event);
  704. if ($event['redirect']) {
  705. $redirect = $event['redirect'];
  706. $redirect_code = $event['redirect_code'];
  707. }
  708. if ($event->isPropagationStopped()) {
  709. break;
  710. }
  711. }
  712. }
  713. if ($legacyUploads) {
  714. $this->copyFiles();
  715. }
  716. $this->getFlash()->delete();
  717. if ($redirect) {
  718. $grav->redirect($redirect, $redirect_code);
  719. }
  720. }
  721. /**
  722. * @return string
  723. * @deprecated 3.0 Use $this->getName() instead
  724. */
  725. public function name(): string
  726. {
  727. return $this->getName();
  728. }
  729. /**
  730. * @return array
  731. * @deprecated 3.0 Use $this->getFields() instead
  732. */
  733. public function fields(): array
  734. {
  735. return $this->getFields();
  736. }
  737. /**
  738. * @return PageInterface
  739. * @deprecated 3.0 Use $this->getPage() instead
  740. */
  741. public function page(): PageInterface
  742. {
  743. return $this->getPage();
  744. }
  745. /**
  746. * Backwards compatibility
  747. *
  748. * @deprecated 3.0
  749. */
  750. public function filter(): void
  751. {
  752. }
  753. /**
  754. * Store form uploads to the final location.
  755. */
  756. public function copyFiles()
  757. {
  758. // Get flash object in order to save the files.
  759. $flash = $this->getFlash();
  760. $fields = $flash->getFilesByFields();
  761. foreach ($fields as $key => $uploads) {
  762. /** @var FormFlashFile $upload */
  763. foreach ($uploads as $upload) {
  764. if (null === $upload || $upload->isMoved()) {
  765. continue;
  766. }
  767. $destination = $upload->getDestination();
  768. $filesystem = Filesystem::getInstance();
  769. $folder = $filesystem->dirname($destination);
  770. if (!is_dir($folder) && !@mkdir($folder, 0777, true) && !is_dir($folder)) {
  771. $grav = Grav::instance();
  772. throw new \RuntimeException(sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '"' . $upload->getClientFilename() . '"', $destination));
  773. }
  774. try {
  775. $upload->moveTo($destination);
  776. } catch (\RuntimeException $e) {
  777. $grav = Grav::instance();
  778. throw new \RuntimeException(sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '"' . $upload->getClientFilename() . '"', $destination));
  779. }
  780. }
  781. }
  782. $flash->clearFiles();
  783. }
  784. public function legacyUploads()
  785. {
  786. // Get flash object in order to save the files.
  787. $flash = $this->getFlash();
  788. $queue = $verify = $flash->getLegacyFiles();
  789. if (!$queue) {
  790. return;
  791. }
  792. $grav = Grav::instance();
  793. /** @var Uri $uri */
  794. $uri = $grav['uri'];
  795. // Get POST data and decode JSON fields into arrays
  796. $post = $uri->post();
  797. $post['data'] = $this->decodeData($post['data'] ?? []);
  798. // Allow plugins to implement additional / alternative logic
  799. $grav->fireEvent('onFormStoreUploads', new Event(['form' => $this, 'queue' => &$queue, 'post' => $post]));
  800. $modified = $queue !== $verify;
  801. if (!$modified) {
  802. // Fill file fields just like before.
  803. foreach ($queue as $key => $files) {
  804. foreach ($files as $destination => $file) {
  805. unset($files[$destination]['tmp_name']);
  806. }
  807. $this->setImageField($key, $files);
  808. }
  809. } else {
  810. user_error('Event onFormStoreUploads is deprecated.', E_USER_DEPRECATED);
  811. if (\is_array($queue)) {
  812. foreach ($queue as $key => $files) {
  813. foreach ($files as $destination => $file) {
  814. $filesystem = Filesystem::getInstance();
  815. $folder = $filesystem->dirname($destination);
  816. if (!is_dir($folder) && !@mkdir($folder, 0777, true) && !is_dir($folder)) {
  817. $grav = Grav::instance();
  818. throw new \RuntimeException(sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '"' . $file['tmp_name'] . '"', $destination));
  819. }
  820. if (!rename($file['tmp_name'], $destination)) {
  821. $grav = Grav::instance();
  822. throw new \RuntimeException(sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '"' . $file['tmp_name'] . '"', $destination));
  823. }
  824. if (file_exists($file['tmp_name'] . '.yaml')) {
  825. unlink($file['tmp_name'] . '.yaml');
  826. }
  827. unset($files[$destination]['tmp_name']);
  828. }
  829. $this->setImageField($key, $files);
  830. }
  831. }
  832. $flash->clearFiles();
  833. }
  834. }
  835. public function getPagePathFromToken($path)
  836. {
  837. return Utils::getPagePathFromToken($path, $this->page());
  838. }
  839. /**
  840. * @return Route|null
  841. */
  842. public function getFileUploadAjaxRoute(): ?Route
  843. {
  844. $route = Uri::getCurrentRoute()->withExtension('json')->withGravParam('task', 'file-upload');
  845. return $route;
  846. }
  847. /**
  848. * @param $field
  849. * @param $filename
  850. * @return Route|null
  851. */
  852. public function getFileDeleteAjaxRoute($field, $filename): ?Route
  853. {
  854. $route = Uri::getCurrentRoute()->withExtension('json')->withGravParam('task', 'file-remove');
  855. return $route;
  856. }
  857. public function responseCode($code = null)
  858. {
  859. if ($code) {
  860. $this->response_code = $code;
  861. }
  862. return $this->response_code;
  863. }
  864. public function doSerialize()
  865. {
  866. return $this->doTraitSerialize() + [
  867. 'items' => $this->items,
  868. 'message' => $this->message,
  869. 'status' => $this->status,
  870. 'header_data' => $this->header_data,
  871. 'rules' => $this->rules,
  872. 'values' => $this->values->toArray(),
  873. 'page' => $this->page
  874. ];
  875. }
  876. public function doUnserialize(array $data)
  877. {
  878. $this->items = $data['items'];
  879. $this->message = $data['message'];
  880. $this->status = $data['status'];
  881. $this->header_data = $data['header_data'];
  882. $this->rules = $data['rules'];
  883. $this->values = new Data($data['values']);
  884. $this->page = $data['page'];
  885. // Backwards compatibility.
  886. $defaults = [
  887. 'name' => $this->items['name'],
  888. 'id' => $this->items['id'],
  889. 'uniqueid' => $this->items['uniqueid'] ?? null,
  890. 'data' => []
  891. ];
  892. $this->doTraitUnserialize($data + $defaults);
  893. }
  894. /**
  895. * Get the configured max file size in bytes
  896. *
  897. * @param bool $mbytes return size in MB
  898. * @return int
  899. */
  900. public static function getMaxFilesize($mbytes = false)
  901. {
  902. $config = Grav::instance()['config'];
  903. $system_filesize = 0;
  904. $form_filesize = $config->get('plugins.form.files.filesize', 0);
  905. $upload_limit = (int) Utils::getUploadLimit();
  906. if ($upload_limit > 0) {
  907. $system_filesize = intval($upload_limit / static::BYTES_TO_MB);
  908. }
  909. if ($form_filesize > $system_filesize || $form_filesize == 0) {
  910. $form_filesize = $system_filesize;
  911. }
  912. if ($mbytes) {
  913. return $form_filesize * static::BYTES_TO_MB;
  914. }
  915. return $form_filesize;
  916. }
  917. protected function sendJsonResponse(callable $callable)
  918. {
  919. $grav = Grav::instance();
  920. /** @var Uri $uri */
  921. $uri = $grav['uri'];
  922. // Get POST data and decode JSON fields into arrays
  923. $post = $uri->post();
  924. $post['data'] = $this->decodeData($post['data'] ?? []);
  925. if (empty($post['form-nonce']) || !Utils::verifyNonce($post['form-nonce'], 'form')) {
  926. throw new \RuntimeException('Bad Request: Nonce is missing or invalid', 400);
  927. }
  928. $this->values = new Data($post);
  929. $json_response = $callable($post);
  930. // Return JSON
  931. header('Content-Type: application/json');
  932. echo json_encode($json_response);
  933. exit;
  934. }
  935. /**
  936. * Remove uploaded file from flash object.
  937. *
  938. * @param string $filename
  939. * @param string|null $field
  940. */
  941. protected function removeFlashUpload(string $filename, string $field = null)
  942. {
  943. $flash = $this->getFlash();
  944. $flash->removeFile($filename, $field);
  945. $flash->save();
  946. }
  947. /**
  948. * Store updated data into flash object.
  949. *
  950. * @param array $data
  951. */
  952. protected function updateFlashData(array $data)
  953. {
  954. // Store updated data into flash.
  955. $flash = $this->getFlash();
  956. // Check special case where there are no changes made to the form.
  957. if (!$flash->exists() && $data === $this->header_data) {
  958. return;
  959. }
  960. $this->setAllData($flash->getData() ?? []);
  961. $this->data->merge($data);
  962. $flash->setData($this->data->toArray());
  963. $flash->save();
  964. }
  965. protected function doSubmit(array $data, array $files)
  966. {
  967. return;
  968. }
  969. protected function processFields($fields)
  970. {
  971. $types = Grav::instance()['plugins']->formFieldTypes;
  972. $return = [];
  973. foreach ($fields as $key => $value) {
  974. // Default to text if not set
  975. if (!isset($value['type'])) {
  976. $value['type'] = 'text';
  977. }
  978. // Manually merging the field types
  979. if ($types !== null && array_key_exists($value['type'], $types)) {
  980. $value += $types[$value['type']];
  981. }
  982. // Fix numeric indexes
  983. if (is_numeric($key) && isset($value['name'])) {
  984. $key = $value['name'];
  985. }
  986. // Recursively process children
  987. if (isset($value['fields']) && \is_array($value['fields'])) {
  988. $value['fields'] = $this->processFields($value['fields']);
  989. }
  990. $return[$key] = $value;
  991. }
  992. return $return;
  993. }
  994. protected function setImageField($key, $files)
  995. {
  996. $field = $this->data->blueprints()->schema()->get($key);
  997. if (isset($field['type']) && !empty($field['array'])) {
  998. $this->data->set($key, $files);
  999. }
  1000. }
  1001. /**
  1002. * Decode data
  1003. *
  1004. * @param array $data
  1005. * @return array
  1006. */
  1007. protected function decodeData($data)
  1008. {
  1009. if (!\is_array($data)) {
  1010. return [];
  1011. }
  1012. // Decode JSON encoded fields and merge them to data.
  1013. if (isset($data['_json'])) {
  1014. $data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
  1015. unset($data['_json']);
  1016. }
  1017. $data = $this->cleanDataKeys($data);
  1018. return $data;
  1019. }
  1020. /**
  1021. * Decode [] in the data keys
  1022. *
  1023. * @param array $source
  1024. * @return array
  1025. */
  1026. protected function cleanDataKeys($source = [])
  1027. {
  1028. $out = [];
  1029. if (\is_array($source)) {
  1030. foreach ($source as $key => $value) {
  1031. $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
  1032. if (\is_array($value)) {
  1033. $out[$key] = $this->cleanDataKeys($value);
  1034. } else {
  1035. $out[$key] = $value;
  1036. }
  1037. }
  1038. }
  1039. return $out;
  1040. }
  1041. /**
  1042. * Internal method to normalize the $_FILES array
  1043. *
  1044. * @param array $data $_FILES starting point data
  1045. * @param string $key
  1046. * @return object a new Object with a normalized list of files
  1047. */
  1048. protected function normalizeFiles($data, $key = '')
  1049. {
  1050. $files = new \stdClass();
  1051. $files->field = $key;
  1052. $files->file = new \stdClass();
  1053. foreach ($data as $fieldName => $fieldValue) {
  1054. // Since Files Upload are always happening via Ajax
  1055. // we are not interested in handling `multiple="true"`
  1056. // because they are always handled one at a time.
  1057. // For this reason we normalize the value to string,
  1058. // in case it is arriving as an array.
  1059. $value = (array) Utils::getDotNotation($fieldValue, $key);
  1060. $files->file->{$fieldName} = array_shift($value);
  1061. }
  1062. return $files;
  1063. }
  1064. }