Form.php 37 KB

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