sessionless-1.x-dev/modules/sessionless_forms/src/SessionlessFormCache.php
modules/sessionless_forms/src/SessionlessFormCache.php
<?php
namespace Drupal\sessionless_forms;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormCacheInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Sessionless form cache.
*
* Copy of FormCache, but storing in encrypted form fields.
*
* @see \Drupal\Core\Form\FormCache
* @see \Drupal\Core\Form\FormBuilder
* @see \sessionless_forms_form_alter
*/
class SessionlessFormCache implements FormCacheInterface {
public function __construct(
protected string $root,
protected ModuleHandlerInterface $moduleHandler,
protected AccountInterface $currentUser,
protected CsrfTokenGenerator $csrfToken,
protected LoggerChannelInterface $logger,
protected SessionlessFormHiddenFieldHandler $sessionlessFormLazyBuilder,
) {}
public function getCache($form_build_id, FormStateInterface $form_state) {
$form = NULL;
$userInput = $form_state->getUserInput();
if ($storedForm = $this->sessionlessFormLazyBuilder->getForm($userInput)) {
$form = $storedForm;
if ($stored_form_state = $this->sessionlessFormLazyBuilder->getFormStateArray($userInput)) {
// Re-populate $form_state for subsequent rebuilds.
$form_state->setFormState($stored_form_state);
// If the original form is contained in include files, load the files.
// @see \Drupal\Core\Form\FormStateInterface::loadInclude()
$build_info = $form_state->getBuildInfo();
$build_info += ['files' => []];
foreach ($build_info['files'] as $file) {
if (is_array($file)) {
$file += ['type' => 'inc', 'name' => $file['module']];
$this->moduleHandler->loadInclude($file['module'], $file['type'], $file['name']);
}
elseif (file_exists($file)) {
require_once $this->root . '/' . $file;
}
}
}
// Generate a new #build_id if the cached form was rendered on a
// cacheable page.
$build_info = $form_state->getBuildInfo();
if (!empty($build_info['immutable'])) {
$form['#build_id_old'] = $form['#build_id'];
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
$form['form_build_id']['#value'] = $form['#build_id'];
$form['form_build_id']['#id'] = $form['#build_id'];
unset($build_info['immutable']);
$form_state->setBuildInfo($build_info);
}
}
return $form;
}
/**
* Set cache.
*
* Called in FormBuilder::setCache() (L458), which is called in:
* - FormBuilder::rebuildForm(L436), which runs after ::prepareForm, which
* invokes form_alter hooks
* - FormBuilder::processForm(L640) storing $unprocessedForm
* - which runs after ::prepareForm when called in ::buildForm()(L320)
* - which runs after ::prepareForm when called in ::submitForm()(L498)
* So we can always assume that hook_form_alter ran before this, and rely on
* the hack used there.
*/
public function setCache($form_build_id, $form, FormStateInterface $form_state) {
if (isset($form['#build_id']) && $form['#build_id'] != $form_build_id) {
$this->logger->error('Form build-id mismatch detected while attempting to store a form in the cache.');
return;
}
// Cache form structure.
if (isset($form)) {
if ($this->currentUser->isAuthenticated()) {
$form['#cache_token'] = $this->csrfToken->get();
}
unset($form['#build_id_old']);
$this->sessionlessFormLazyBuilder->setForm($form_state, $form);
}
if ($form_state->getCacheableArray()) {
$this->sessionlessFormLazyBuilder->setFormState($form_state);
}
}
public function deleteCache($form_build_id) {}
}
