Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

Form.php 40 KiB

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