form.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  1. <?php
  2. namespace Grav\Plugin;
  3. use Composer\Autoload\ClassLoader;
  4. use Grav\Common\Data\ValidationException;
  5. use Grav\Common\Filesystem\Folder;
  6. use Grav\Common\Page\Page;
  7. use Grav\Common\Page\Pages;
  8. use Grav\Common\Page\Types;
  9. use Grav\Common\Plugin;
  10. use Grav\Common\Twig\Twig;
  11. use Grav\Common\Utils;
  12. use Grav\Common\Uri;
  13. use Grav\Plugin\Form\Form;
  14. use RocketTheme\Toolbox\File\JsonFile;
  15. use RocketTheme\Toolbox\File\YamlFile;
  16. use Symfony\Component\Yaml\Yaml;
  17. use RocketTheme\Toolbox\File\File;
  18. use RocketTheme\Toolbox\Event\Event;
  19. /**
  20. * Class FormPlugin
  21. * @package Grav\Plugin
  22. */
  23. class FormPlugin extends Plugin
  24. {
  25. /** @var array */
  26. public $features = [
  27. 'blueprints' => 1000
  28. ];
  29. /** @var Form */
  30. protected $form;
  31. /** @var array */
  32. protected $forms = [];
  33. /** @var array */
  34. protected $flat_forms = [];
  35. /** @var array */
  36. protected $json_response = [];
  37. /** @var bool */
  38. protected $recache_forms = false;
  39. /**
  40. * @return array
  41. */
  42. public static function getSubscribedEvents()
  43. {
  44. return [
  45. 'onPluginsInitialized' => [
  46. ['autoload', 100000],
  47. ['onPluginsInitialized', 0]
  48. ],
  49. 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0]
  50. ];
  51. }
  52. /**
  53. * [onPluginsInitialized:100000] Composer autoload.
  54. *
  55. * @return ClassLoader
  56. */
  57. public function autoload()
  58. {
  59. return require __DIR__ . '/vendor/autoload.php';
  60. }
  61. /**
  62. * Initialize forms from cache if possible
  63. */
  64. public function onPluginsInitialized()
  65. {
  66. // Backwards compatibility for plugins that use forms.
  67. class_alias(Form::class, 'Grav\Plugin\Form');
  68. if ($this->isAdmin()) {
  69. $this->enable([
  70. 'onPageInitialized' => ['onPageInitialized', 0],
  71. 'onGetPageTemplates' => ['onGetPageTemplates', 0],
  72. ]);
  73. return;
  74. }
  75. $this->enable([
  76. 'onPageProcessed' => ['onPageProcessed', 0],
  77. 'onPagesInitialized' => ['onPagesInitialized', 0],
  78. 'onPageInitialized' => ['onPageInitialized', 0],
  79. 'onTwigInitialized' => ['onTwigInitialized', 0],
  80. 'onTwigPageVariables' => ['onTwigVariables', 0],
  81. 'onTwigSiteVariables' => ['onTwigVariables', 0],
  82. 'onFormValidationProcessed' => ['onFormValidationProcessed', 0],
  83. ]);
  84. }
  85. public function onGetPageTemplates(Event $event)
  86. {
  87. /** @var Types $types */
  88. $types = $event->types;
  89. $types->register('form');
  90. }
  91. /**
  92. * Process forms after page header processing, but before caching
  93. *
  94. * @param Event $e
  95. */
  96. public function onPageProcessed(Event $e)
  97. {
  98. /** @var Page $page */
  99. $page = $e['page'];
  100. $page_route = $page->route();
  101. if ($page->home()) {
  102. $page_route = '/';
  103. }
  104. $header = $page->header();
  105. // Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
  106. $this->grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $page, 'header' => $header]));
  107. if ((isset($header->forms) && is_array($header->forms)) ||
  108. (isset($header->form) && is_array($header->form))) {
  109. $page_forms = [];
  110. // Force never_cache_twig if modular form
  111. if ($page->modular()) {
  112. $header->never_cache_twig = true;
  113. }
  114. // Get the forms from the page headers
  115. if (isset($header->forms)) {
  116. $page_forms = $header->forms;
  117. } elseif (isset($header->form)) {
  118. $page_forms[] = $header->form;
  119. }
  120. // Store the page forms in the forms instance
  121. foreach ($page_forms as $name => $page_form) {
  122. $form = new Form($page, $name, $page_form);
  123. $this->addForm($page_route, $form);
  124. }
  125. }
  126. }
  127. /**
  128. * Initialize all the forms
  129. */
  130. public function onPagesInitialized()
  131. {
  132. $this->loadCachedForms();
  133. }
  134. /**
  135. * Catches form processing if user posts the form.
  136. */
  137. public function onPageInitialized()
  138. {
  139. $submitted = false;
  140. $this->json_response = [];
  141. // Save cached forms.
  142. if ($this->recache_forms) {
  143. $this->saveCachedForms();
  144. }
  145. // Force rebuild form when form has not been built and form cache expired.
  146. // This happens when form cache expires before the page cache
  147. // and then does not trigger 'onPageProcessed' event.
  148. if (!$this->forms) {
  149. $this->onPageProcessed(new Event(['page' => $this->grav['page']]));
  150. }
  151. // Enable form events if there's a POST
  152. if ($this->shouldProcessForm()) {
  153. $this->enable([
  154. 'onFormProcessed' => ['onFormProcessed', 0],
  155. 'onFormValidationError' => ['onFormValidationError', 0],
  156. 'onFormFieldTypes' => ['onFormFieldTypes', 0],
  157. ]);
  158. $uri = $this->grav['uri'];
  159. // Post the form
  160. if ($this->form) {
  161. if ($uri->post('__form-file-uploader__') && $uri->extension() === 'json') {
  162. $this->json_response = $this->form->uploadFiles();
  163. } else if ($this->form && isset($_POST['__form-file-remover__']) && $this->grav['uri']->extension() === 'json') {
  164. $this->json_response = $this->form->filesSessionRemove();
  165. } else {
  166. $this->form->post();
  167. $submitted = true;
  168. }
  169. }
  170. // Clear flash objects for previously uploaded files
  171. // whenever the user switches page / reloads
  172. // ignoring any JSON / extension call
  173. if (!$submitted && null === $uri->extension()) {
  174. // Discard any previously uploaded files session.
  175. // and if there were any uploaded file, remove them from the filesystem
  176. if ($flash = $this->grav['session']->getFlashObject('files-upload')) {
  177. $flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
  178. foreach ($flash as $key => $value) {
  179. if ($key !== 'tmp_name') {
  180. continue;
  181. }
  182. @unlink($value);
  183. }
  184. }
  185. }
  186. }
  187. }
  188. /**
  189. * Add simple `forms()` Twig function
  190. */
  191. public function onTwigInitialized()
  192. {
  193. $this->grav['twig']->twig()->addFunction(
  194. new \Twig_SimpleFunction('forms', [$this, 'getForm'])
  195. );
  196. $this->grav['twig']->twig()->getExtension('Twig_Extension_Core')->setEscaper('yaml', function($twig, $string, $charset) {
  197. return Yaml::dump($string);
  198. }
  199. );
  200. }
  201. /**
  202. * Add current directory to twig lookup paths.
  203. */
  204. public function onTwigTemplatePaths()
  205. {
  206. $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
  207. }
  208. /**
  209. * Make form accessible from twig.
  210. *
  211. * @param Event $event
  212. */
  213. public function onTwigVariables(Event $event = null)
  214. {
  215. if ($event !== null && isset($event['page'])) {
  216. $page = $event['page'];
  217. } else {
  218. $page = $this->grav['page'];
  219. }
  220. $twig = $this->grav['twig'];
  221. if (!isset($twig->twig_vars['form'])) {
  222. $twig->twig_vars['form'] = $this->form($page);
  223. }
  224. if ($this->config->get('plugins.form.built_in_css')) {
  225. $this->grav['assets']->addCss('plugin://form/assets/form-styles.css');
  226. }
  227. $twig->twig_vars['form_max_filesize'] = Form::getMaxFilesize();
  228. $twig->twig_vars['form_json_response'] = $this->json_response;
  229. }
  230. /**
  231. * Handle form processing instructions.
  232. *
  233. * @param Event $event
  234. * @throws \Exception
  235. */
  236. public function onFormProcessed(Event $event)
  237. {
  238. /** @var Form $form */
  239. $form = $event['form'];
  240. $action = $event['action'];
  241. $params = $event['params'];
  242. $this->process($form);
  243. switch ($action) {
  244. case 'captcha':
  245. if (isset($params['recaptcha_secret'])) {
  246. $recaptchaSecret = $params['recaptcha_secret'];
  247. } elseif (isset($params['recatpcha_secret'])) {
  248. // Included for backwards compatibility with typo (issue #51)
  249. $recaptchaSecret = $params['recatpcha_secret'];
  250. } else {
  251. $recaptchaSecret = $this->config->get('plugins.form.recaptcha.secret_key');
  252. }
  253. // Validate the captcha
  254. $query = http_build_query([
  255. 'secret' => $recaptchaSecret,
  256. 'response' => $form->value('g-recaptcha-response', true)
  257. ]);
  258. $url = 'https://www.google.com/recaptcha/api/siteverify';
  259. if (ini_get('allow_url_fopen')) {
  260. $response = json_decode(file_get_contents($url . '?' . $query), true);
  261. } else {
  262. $ch = curl_init();
  263. curl_setopt($ch, CURLOPT_URL, $url);
  264. curl_setopt($ch, CURLOPT_POST, true);
  265. curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
  266. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  267. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  268. $response = json_decode(curl_exec($ch), true);
  269. }
  270. if (!isset($response['success']) || $response['success'] !== true) {
  271. $this->grav->fireEvent('onFormValidationError', new Event([
  272. 'form' => $form,
  273. 'message' => $this->grav['language']->translate('PLUGIN_FORM.ERROR_VALIDATING_CAPTCHA')
  274. ]));
  275. $event->stopPropagation();
  276. return;
  277. }
  278. break;
  279. case 'timestamp':
  280. $label = isset($params['label']) ? $params['label'] : 'Timestamp';
  281. $format = isset($params['format']) ? $params['format'] : 'Y-m-d H:i:s';
  282. $blueprint = $form->value()->blueprints();
  283. $blueprint->set('form/fields/timestamp', ['name'=>'timestamp', 'label'=> $label]);
  284. $now = new \DateTime('now');
  285. $date_string = $now->format($format);
  286. $form->setFields($blueprint->fields());
  287. $form->setData('timestamp',$date_string);
  288. break;
  289. case 'ip':
  290. $label = isset($params['label']) ? $params['label'] : 'User IP';
  291. $blueprint = $form->value()->blueprints();
  292. $blueprint->set('form/fields/ip', ['name'=>'ip', 'label'=> $label]);
  293. $form->setFields($blueprint->fields());
  294. $form->setData('ip', Uri::ip());
  295. break;
  296. case 'message':
  297. $translated_string = $this->grav['language']->translate($params);
  298. $vars = array(
  299. 'form' => $form
  300. );
  301. /** @var Twig $twig */
  302. $twig = $this->grav['twig'];
  303. $processed_string = $twig->processString($translated_string, $vars);
  304. $form->message = $processed_string;
  305. break;
  306. case 'redirect':
  307. $this->grav['session']->setFlashObject('form', $form);
  308. $url = ((string)$params);
  309. $vars = array(
  310. 'form' => $form
  311. );
  312. /** @var Twig $twig */
  313. $twig = $this->grav['twig'];
  314. $url = $twig->processString($url, $vars);
  315. $this->grav->redirect($url);
  316. break;
  317. case 'reset':
  318. if (Utils::isPositive($params)) {
  319. $form->reset();
  320. }
  321. break;
  322. case 'display':
  323. $route = (string)$params;
  324. if (!$route || $route[0] !== '/') {
  325. /** @var Uri $uri */
  326. $uri = $this->grav['uri'];
  327. $route = rtrim($uri->route(), '/'). '/' . ($route ?: '');
  328. }
  329. /** @var Twig $twig */
  330. $twig = $this->grav['twig'];
  331. $twig->twig_vars['form'] = $form;
  332. /** @var Pages $pages */
  333. $pages = $this->grav['pages'];
  334. $page = $pages->dispatch($route, true);
  335. if (!$page) {
  336. throw new \RuntimeException('Display page not found. Please check the page exists.', 400);
  337. }
  338. unset($this->grav['page']);
  339. $this->grav['page'] = $page;
  340. break;
  341. case 'remember':
  342. foreach ($params as $remember_field) {
  343. $field_cookie = 'forms-'.$form['name'].'-'.$remember_field;
  344. setcookie($field_cookie, $form->value($remember_field), time()+60*60*24*60);
  345. }
  346. break;
  347. case 'save':
  348. $prefix = !empty($params['fileprefix']) ? $params['fileprefix'] : '';
  349. $format = !empty($params['dateformat']) ? $params['dateformat'] : 'Ymd-His-u';
  350. $raw_format = !empty($params['dateraw']) ? (bool) $params['dateraw'] : false;
  351. $postfix = !empty($params['filepostfix']) ? $params['filepostfix'] : '';
  352. $ext = !empty($params['extension']) ? '.' . trim($params['extension'], '.') : '.txt';
  353. $filename = !empty($params['filename']) ? $params['filename'] : '';
  354. $operation = !empty($params['operation']) ? $params['operation'] : 'create';
  355. if (!$filename) {
  356. $filename = $prefix . $this->udate($format, $raw_format) . $postfix. $ext;
  357. }
  358. /** @var Twig $twig */
  359. $twig = $this->grav['twig'];
  360. $vars = [
  361. 'form' => $form
  362. ];
  363. // Process with Twig
  364. $filename = $twig->processString($filename, $vars);
  365. $locator = $this->grav['locator'];
  366. $path = $locator->findResource('user://data', true);
  367. $dir = $path . DS . $form->name();
  368. $fullFileName = $dir. DS . $filename;
  369. if (!empty($params['raw']) || !empty($params['template'])) {
  370. // Save data as it comes from the form.
  371. if ($operation === 'add') {
  372. throw new \RuntimeException('Form save: \'operation: add\' is not supported for raw files');
  373. }
  374. switch ($ext) {
  375. case '.yaml':
  376. $file = YamlFile::instance($fullFileName);
  377. break;
  378. case '.json':
  379. $file = JsonFile::instance($fullFileName);
  380. break;
  381. default:
  382. throw new \RuntimeException('Form save: Unsupported RAW file format, please use either yaml or json');
  383. }
  384. $data = [
  385. '_data_type' => 'form',
  386. 'template' => !empty($params['template']) ? $params['template'] : null,
  387. 'name' => $form->name(),
  388. 'timestamp' => date('Y-m-d H:i:s'),
  389. 'content' => $form->getData()->toArray()
  390. ];
  391. $file->save(array_filter($data));
  392. break;
  393. }
  394. $file = File::instance($fullFileName);
  395. if ($operation === 'create') {
  396. $body = $twig->processString(!empty($params['body']) ? $params['body'] : '{% include "forms/data.txt.twig" %}',
  397. $vars);
  398. $file->save($body);
  399. } elseif ($operation === 'add') {
  400. if (!empty($params['body'])) {
  401. // use body similar to 'create' action and append to file as a log
  402. $body = $twig->processString($params['body'], $vars);
  403. // create folder if it doesn't exist
  404. if (!file_exists($dir)) {
  405. Folder::create($dir);
  406. }
  407. // append data to existing file
  408. file_put_contents($fullFileName, $body, FILE_APPEND | LOCK_EX);
  409. } else {
  410. // serialize YAML out to file for easier parsing as data sets
  411. $vars = $vars['form']->value()->toArray();
  412. foreach ($form->fields as $field) {
  413. if (!empty($field['process']['ignore'])) {
  414. unset($vars[$field['name']]);
  415. }
  416. }
  417. if (file_exists($fullFileName)) {
  418. $data = Yaml::parse($file->content());
  419. if (count($data) > 0) {
  420. array_unshift($data, $vars);
  421. } else {
  422. $data[] = $vars;
  423. }
  424. } else {
  425. $data[] = $vars;
  426. }
  427. $file->save(Yaml::dump($data));
  428. }
  429. }
  430. break;
  431. case 'call':
  432. $callable = $params;
  433. if (is_array($callable) && !method_exists($callable[0], $callable[1])) {
  434. throw new \RuntimeException('Form cannot be processed (method does not exist)');
  435. }
  436. if (is_string($callable) && !function_exists($callable)) {
  437. throw new \RuntimeException('Form cannot be processed (function does not exist)');
  438. }
  439. call_user_func($callable, $form);
  440. break;
  441. }
  442. }
  443. /**
  444. * Custom field logic can go in here
  445. *
  446. * @param Event $event
  447. */
  448. public function onFormValidationProcessed(Event $event)
  449. {
  450. // special check for honeypot field
  451. foreach ($event['form']->fields() as $field) {
  452. if ($field['type'] === 'honeypot' && !empty($event['form']->value($field['name']))) {
  453. throw new ValidationException('Are you a bot?');
  454. }
  455. }
  456. }
  457. /**
  458. * Handle form validation error
  459. *
  460. * @param Event $event An event object
  461. * @throws \Exception
  462. */
  463. public function onFormValidationError(Event $event)
  464. {
  465. $form = $event['form'];
  466. if (isset($event['message'])) {
  467. $form->status = 'error';
  468. $form->message = $event['message'];
  469. $form->messages = $event['messages'];
  470. }
  471. $uri = $this->grav['uri'];
  472. $route = $uri->route();
  473. /** @var Twig $twig */
  474. $twig = $this->grav['twig'];
  475. $twig->twig_vars['form'] = $form;
  476. /** @var Pages $pages */
  477. $pages = $this->grav['pages'];
  478. $page = $pages->dispatch($route, true);
  479. if ($page) {
  480. unset($this->grav['page']);
  481. $this->grav['page'] = $page;
  482. }
  483. $event->stopPropagation();
  484. }
  485. /**
  486. * Add a form to the forms plugin
  487. *
  488. * @param $page_route
  489. * @param $form
  490. */
  491. public function addForm($page_route, $form)
  492. {
  493. $form_array = [$form['name'] => $form];
  494. if (array_key_exists($page_route, $this->forms)) {
  495. if (!isset($this->form[$page_route][$form['name']])) {
  496. $this->forms[$page_route] = array_merge($this->forms[$page_route], $form_array);
  497. }
  498. } else {
  499. $this->forms[$page_route] = $form_array;
  500. }
  501. $this->flattenForms();
  502. $this->recache_forms = true;
  503. }
  504. /**
  505. * function to get a specific form
  506. *
  507. * @param null|array|string $data optional form `name`
  508. *
  509. * @return null|Form
  510. */
  511. public function getForm($data = null)
  512. {
  513. $page_route = null;
  514. $form_name = null;
  515. if (is_array($data)) {
  516. if (isset($data['name'])) {
  517. $form_name = $data['name'];
  518. }
  519. if (isset($data['route'])) {
  520. $page_route = $data['route'];
  521. }
  522. } elseif (is_string($data)) {
  523. $form_name = $data;
  524. }
  525. // if no form name, use the first form found in the page
  526. if (!$form_name) {
  527. // If page route not provided, use the current page
  528. if (!$page_route) {
  529. // Get page route
  530. $page_route = $this->grav['page']->route();
  531. // fallback using current URI if page not initialized yet
  532. if (!$page_route) {
  533. $page_route = $this->getCurrentPageRoute();
  534. }
  535. }
  536. if (isset($this->forms[$page_route])) {
  537. $forms = $this->forms[$page_route];
  538. $first_form = array_shift($forms);
  539. $form_name = $first_form['name'];
  540. } else {
  541. //No form on this route. Try looking up in the current page first
  542. return new Form($this->grav['page']);
  543. }
  544. }
  545. // return the form you are looking for if available
  546. return $this->getFormByName($form_name);
  547. }
  548. /**
  549. * Get list of form field types specified in this plugin. Only special types needs to be listed.
  550. *
  551. * @return array
  552. */
  553. public function getFormFieldTypes()
  554. {
  555. return [
  556. 'column' => [
  557. 'input@' => false
  558. ],
  559. 'columns' => [
  560. 'input@' => false
  561. ],
  562. 'fieldset' => [
  563. 'input@' => false
  564. ],
  565. 'conditional' => [
  566. 'input@' => false
  567. ],
  568. 'display' => [
  569. 'input@' => false
  570. ],
  571. 'spacer' => [
  572. 'input@' => false
  573. ],
  574. 'captcha' => [
  575. 'input@' => false
  576. ]
  577. ];
  578. }
  579. /**
  580. * Process a form
  581. *
  582. * Currently available processing tasks:
  583. *
  584. * - fillWithCurrentDateTime
  585. *
  586. * @param Form $form
  587. */
  588. protected function process($form)
  589. {
  590. foreach ($form->fields as $field) {
  591. if (!empty($field['process']['fillWithCurrentDateTime'])) {
  592. $form->setData($field['name'], gmdate('D, d M Y H:i:s', time()));
  593. }
  594. }
  595. }
  596. /**
  597. * Get current page's route
  598. *
  599. * @return mixed
  600. */
  601. protected function getCurrentPageRoute()
  602. {
  603. $path = $this->grav['uri']->route();
  604. $path = $path ?: '/';
  605. return $path;
  606. }
  607. /**
  608. * Retrieve a form based on the form name
  609. *
  610. * @param $form_name
  611. * @return mixed
  612. */
  613. protected function getFormByName($form_name)
  614. {
  615. if (array_key_exists($form_name, $this->flat_forms)) {
  616. return $this->flat_forms[$form_name];
  617. }
  618. return null;
  619. }
  620. /**
  621. * Determine if the page has a form submission that should be processed
  622. *
  623. * @return bool
  624. */
  625. protected function shouldProcessForm()
  626. {
  627. $uri = $this->grav['uri'];
  628. $nonce = $uri->post('form-nonce');
  629. $status = $nonce ? true : false; // php72 quirk?
  630. $refresh_prevention = null;
  631. if ($status && $this->form()) {
  632. // Set page template if passed by form
  633. if (isset($this->form->template)) {
  634. $this->grav['page']->template($this->form->template);
  635. }
  636. if (isset($this->form->refresh_prevention)) {
  637. $refresh_prevention = (bool) $this->form->refresh_prevention;
  638. } else {
  639. $refresh_prevention = $this->config->get('plugins.form.refresh_prevention', false);
  640. }
  641. $unique_form_id = $uri->post('__unique_form_id__', FILTER_SANITIZE_STRING);
  642. if ($refresh_prevention && $unique_form_id) {
  643. if ($this->grav['session']->unique_form_id !== $unique_form_id) {
  644. $this->grav['session']->unique_form_id = $unique_form_id;
  645. } else {
  646. $status = false;
  647. $this->form->message = $this->grav['language']->translate('PLUGIN_FORM.FORM_ALREADY_SUBMITTED');
  648. $this->form->status = 'error';
  649. }
  650. }
  651. }
  652. return $status;
  653. }
  654. /**
  655. * Flatten the forms array into something that can be more easily searched
  656. */
  657. protected function flattenForms()
  658. {
  659. $this->flat_forms = Utils::arrayFlatten($this->forms);
  660. }
  661. /**
  662. * Get the current form, should already be processed but can get it directly from the page if necessary
  663. *
  664. * @param Page|null $page
  665. * @return Form|mixed
  666. */
  667. protected function form($page = null)
  668. {
  669. // Regenerate list of flat_forms if not already populated
  670. if (empty($this->flat_forms)) {
  671. $this->flattenForms();
  672. }
  673. if (null === $this->form) {
  674. // try to get the page if possible
  675. if (null === $page) {
  676. $page = $this->grav['page'];
  677. }
  678. $form_name = $this->grav['uri']->post('__form-name__', FILTER_SANITIZE_STRING);
  679. if (!$form_name) {
  680. $form_name = $page ? $page->slug() : null;
  681. }
  682. $this->form = $this->getFormByName($form_name);
  683. // last attempt using current page's form
  684. if (null === $this->form && $page) {
  685. $header = $page->header();
  686. if (isset($header->form)) {
  687. $this->form = new Form($page);
  688. }
  689. }
  690. }
  691. return $this->form;
  692. }
  693. /**
  694. * Load cached forms and merge with any currently found forms
  695. */
  696. protected function loadCachedForms()
  697. {
  698. // Get and set the cache of forms if it exists
  699. list($forms, $flat_forms) = $this->grav['cache']->fetch($this->getFormCacheId());
  700. // Only store the forms if they are an array
  701. if (is_array($forms)) {
  702. $this->forms = array_merge($this->forms, $forms);
  703. }
  704. // Only store the flat_forms if they are an array
  705. if (is_array($flat_forms)) {
  706. $this->flat_forms = array_merge($this->flat_forms, $flat_forms);
  707. }
  708. }
  709. /**
  710. * Save the current state of the forms
  711. */
  712. protected function saveCachedForms()
  713. {
  714. // Save the current state of the forms to cache
  715. if ($this->recache_forms) {
  716. $this->recache_forms = false;
  717. $this->grav['cache']->save($this->getFormCacheId(), [$this->forms, $this->flat_forms]);
  718. }
  719. }
  720. /**
  721. * Get the current page cache based id for the forms cache
  722. *
  723. * @return string
  724. */
  725. protected function getFormCacheId()
  726. {
  727. return $this->grav['pages']->getPagesCacheId() . '-form-plugin';
  728. }
  729. /**
  730. * Create unix timestamp for storing the data into the filesystem.
  731. *
  732. * @param string $format
  733. * @param bool $raw
  734. *
  735. * @return string
  736. */
  737. protected function udate($format = 'u', $raw = false)
  738. {
  739. $utimestamp = microtime(true);
  740. if ($raw) {
  741. return date($format);
  742. }
  743. $timestamp = floor($utimestamp);
  744. $milliseconds = round(($utimestamp - $timestamp) * 1000000);
  745. return date(preg_replace('`(?<!\\\\)u`', \sprintf('%06d', $milliseconds), $format), $timestamp);
  746. }
  747. }