3 namespace Drupal\Core\Form;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Access\CsrfTokenGenerator;
7 use Drupal\Core\Extension\ModuleHandlerInterface;
8 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
9 use Drupal\Core\PageCache\RequestPolicyInterface;
10 use Drupal\Core\Session\AccountInterface;
11 use Drupal\Core\Site\Settings;
12 use Psr\Log\LoggerInterface;
13 use Symfony\Component\HttpFoundation\RequestStack;
16 * Encapsulates the caching of a form and its form state.
20 class FormCache implements FormCacheInterface {
23 * The factory for expirable key value stores used by form cache.
25 * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
27 protected $keyValueExpirableFactory;
30 * The CSRF token generator to validate the form token.
32 * @var \Drupal\Core\Access\CsrfTokenGenerator
39 * @var \Drupal\Core\Session\AccountInterface
41 protected $currentUser;
46 * @var \Drupal\Core\Extension\ModuleHandlerInterface
48 protected $moduleHandler;
53 * @var \Drupal\Core\Logger\LoggerChannelInterface
60 * @var \Drupal\Core\Config\ConfigFactoryInterface
62 protected $configFactory;
67 * @var \Symfony\Component\HttpFoundation\RequestStack
69 protected $requestStack;
72 * A policy rule determining the cacheability of a request.
74 * @var \Drupal\Core\PageCache\RequestPolicyInterface
76 protected $requestPolicy;
86 * Constructs a new FormCache.
90 * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
91 * The key value expirable factory, used to create key value expirable
92 * stores for the form cache and form state cache.
93 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
95 * @param \Drupal\Core\Session\AccountInterface $current_user
97 * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
98 * The CSRF token generator.
99 * @param \Psr\Log\LoggerInterface $logger
101 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
103 * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
104 * A policy rule determining the cacheability of a request.
106 public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, ModuleHandlerInterface $module_handler, AccountInterface $current_user, CsrfTokenGenerator $csrf_token, LoggerInterface $logger, RequestStack $request_stack, RequestPolicyInterface $request_policy) {
108 $this->keyValueExpirableFactory = $key_value_expirable_factory;
109 $this->moduleHandler = $module_handler;
110 $this->currentUser = $current_user;
111 $this->logger = $logger;
112 $this->csrfToken = $csrf_token;
113 $this->requestStack = $request_stack;
114 $this->requestPolicy = $request_policy;
120 public function getCache($form_build_id, FormStateInterface $form_state) {
121 if ($form = $this->keyValueExpirableFactory->get('form')->get($form_build_id)) {
122 if ((isset($form['#cache_token']) && $this->csrfToken->validate($form['#cache_token'])) || (!isset($form['#cache_token']) && $this->currentUser->isAnonymous())) {
123 $this->loadCachedFormState($form_build_id, $form_state);
125 // Generate a new #build_id if the cached form was rendered on a
127 $build_info = $form_state->getBuildInfo();
128 if (!empty($build_info['immutable'])) {
129 $form['#build_id_old'] = $form['#build_id'];
130 $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
131 $form['form_build_id']['#value'] = $form['#build_id'];
132 $form['form_build_id']['#id'] = $form['#build_id'];
133 unset($build_info['immutable']);
134 $form_state->setBuildInfo($build_info);
142 * Loads the cached form state.
144 * @param string $form_build_id
145 * The unique form build ID.
146 * @param \Drupal\Core\Form\FormStateInterface $form_state
147 * The current state of the form.
149 protected function loadCachedFormState($form_build_id, FormStateInterface $form_state) {
150 if ($stored_form_state = $this->keyValueExpirableFactory->get('form_state')->get($form_build_id)) {
151 // Re-populate $form_state for subsequent rebuilds.
152 $form_state->setFormState($stored_form_state);
154 // If the original form is contained in include files, load the files.
155 // @see \Drupal\Core\Form\FormStateInterface::loadInclude()
156 $build_info = $form_state->getBuildInfo();
157 $build_info += ['files' => []];
158 foreach ($build_info['files'] as $file) {
159 if (is_array($file)) {
160 $file += ['type' => 'inc', 'name' => $file['module']];
161 $this->moduleHandler->loadInclude($file['module'], $file['type'], $file['name']);
163 elseif (file_exists($file)) {
164 require_once $this->root . '/' . $file;
173 public function setCache($form_build_id, $form, FormStateInterface $form_state) {
174 // Cache forms for 6 hours by default.
175 $expire = Settings::get('form_cache_expiration', 21600);
177 // Ensure that the form build_id embedded in the form structure is the same
178 // as the one passed in as a parameter. This is an additional safety measure
179 // to prevent legacy code operating directly with
180 // \Drupal::formBuilder()->getCache() and \Drupal::formBuilder()->setCache()
181 // from accidentally overwriting immutable form state.
182 if (isset($form['#build_id']) && $form['#build_id'] != $form_build_id) {
183 $this->logger->error('Form build-id mismatch detected while attempting to store a form in the cache.');
187 // Cache form structure.
189 if ($this->currentUser->isAuthenticated()) {
190 $form['#cache_token'] = $this->csrfToken->get();
192 unset($form['#build_id_old']);
193 $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire);
196 if ($data = $form_state->getCacheableArray()) {
197 $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire);
204 public function deleteCache($form_build_id) {
205 $this->keyValueExpirableFactory->get('form')->delete($form_build_id);
206 $this->keyValueExpirableFactory->get('form_state')->delete($form_build_id);