FormCache.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. namespace Drupal\Core\Form;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Access\CsrfTokenGenerator;
  5. use Drupal\Core\Extension\ModuleHandlerInterface;
  6. use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
  7. use Drupal\Core\PageCache\RequestPolicyInterface;
  8. use Drupal\Core\Session\AccountInterface;
  9. use Drupal\Core\Site\Settings;
  10. use Psr\Log\LoggerInterface;
  11. use Symfony\Component\HttpFoundation\RequestStack;
  12. /**
  13. * Encapsulates the caching of a form and its form state.
  14. *
  15. * @ingroup form_api
  16. */
  17. class FormCache implements FormCacheInterface {
  18. /**
  19. * The factory for expirable key value stores used by form cache.
  20. *
  21. * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
  22. */
  23. protected $keyValueExpirableFactory;
  24. /**
  25. * The CSRF token generator to validate the form token.
  26. *
  27. * @var \Drupal\Core\Access\CsrfTokenGenerator
  28. */
  29. protected $csrfToken;
  30. /**
  31. * The current user.
  32. *
  33. * @var \Drupal\Core\Session\AccountInterface
  34. */
  35. protected $currentUser;
  36. /**
  37. * The module handler.
  38. *
  39. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  40. */
  41. protected $moduleHandler;
  42. /**
  43. * Logger channel.
  44. *
  45. * @var \Drupal\Core\Logger\LoggerChannelInterface
  46. */
  47. protected $logger;
  48. /**
  49. * The config factory.
  50. *
  51. * @var \Drupal\Core\Config\ConfigFactoryInterface
  52. */
  53. protected $configFactory;
  54. /**
  55. * The request stack.
  56. *
  57. * @var \Symfony\Component\HttpFoundation\RequestStack
  58. */
  59. protected $requestStack;
  60. /**
  61. * A policy rule determining the cacheability of a request.
  62. *
  63. * @var \Drupal\Core\PageCache\RequestPolicyInterface
  64. */
  65. protected $requestPolicy;
  66. /**
  67. * The app root.
  68. *
  69. * @var string
  70. */
  71. protected $root;
  72. /**
  73. * Constructs a new FormCache.
  74. *
  75. * @param string $root
  76. * The app root.
  77. * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
  78. * The key value expirable factory, used to create key value expirable
  79. * stores for the form cache and form state cache.
  80. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  81. * The module handler.
  82. * @param \Drupal\Core\Session\AccountInterface $current_user
  83. * The current user.
  84. * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
  85. * The CSRF token generator.
  86. * @param \Psr\Log\LoggerInterface $logger
  87. * A logger instance.
  88. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  89. * The request stack.
  90. * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
  91. * A policy rule determining the cacheability of a request.
  92. */
  93. 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) {
  94. $this->root = $root;
  95. $this->keyValueExpirableFactory = $key_value_expirable_factory;
  96. $this->moduleHandler = $module_handler;
  97. $this->currentUser = $current_user;
  98. $this->logger = $logger;
  99. $this->csrfToken = $csrf_token;
  100. $this->requestStack = $request_stack;
  101. $this->requestPolicy = $request_policy;
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. public function getCache($form_build_id, FormStateInterface $form_state) {
  107. if ($form = $this->keyValueExpirableFactory->get('form')->get($form_build_id)) {
  108. if ((isset($form['#cache_token']) && $this->csrfToken->validate($form['#cache_token'])) || (!isset($form['#cache_token']) && $this->currentUser->isAnonymous())) {
  109. $this->loadCachedFormState($form_build_id, $form_state);
  110. // Generate a new #build_id if the cached form was rendered on a
  111. // cacheable page.
  112. $build_info = $form_state->getBuildInfo();
  113. if (!empty($build_info['immutable'])) {
  114. $form['#build_id_old'] = $form['#build_id'];
  115. $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
  116. $form['form_build_id']['#value'] = $form['#build_id'];
  117. $form['form_build_id']['#id'] = $form['#build_id'];
  118. unset($build_info['immutable']);
  119. $form_state->setBuildInfo($build_info);
  120. }
  121. return $form;
  122. }
  123. }
  124. }
  125. /**
  126. * Loads the cached form state.
  127. *
  128. * @param string $form_build_id
  129. * The unique form build ID.
  130. * @param \Drupal\Core\Form\FormStateInterface $form_state
  131. * The current state of the form.
  132. */
  133. protected function loadCachedFormState($form_build_id, FormStateInterface $form_state) {
  134. if ($stored_form_state = $this->keyValueExpirableFactory->get('form_state')->get($form_build_id)) {
  135. // Re-populate $form_state for subsequent rebuilds.
  136. $form_state->setFormState($stored_form_state);
  137. // If the original form is contained in include files, load the files.
  138. // @see \Drupal\Core\Form\FormStateInterface::loadInclude()
  139. $build_info = $form_state->getBuildInfo();
  140. $build_info += ['files' => []];
  141. foreach ($build_info['files'] as $file) {
  142. if (is_array($file)) {
  143. $file += ['type' => 'inc', 'name' => $file['module']];
  144. $this->moduleHandler->loadInclude($file['module'], $file['type'], $file['name']);
  145. }
  146. elseif (file_exists($file)) {
  147. require_once $this->root . '/' . $file;
  148. }
  149. }
  150. }
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. public function setCache($form_build_id, $form, FormStateInterface $form_state) {
  156. // Cache forms for 6 hours by default.
  157. $expire = Settings::get('form_cache_expiration', 21600);
  158. // Ensure that the form build_id embedded in the form structure is the same
  159. // as the one passed in as a parameter. This is an additional safety measure
  160. // to prevent legacy code operating directly with
  161. // \Drupal::formBuilder()->getCache() and \Drupal::formBuilder()->setCache()
  162. // from accidentally overwriting immutable form state.
  163. if (isset($form['#build_id']) && $form['#build_id'] != $form_build_id) {
  164. $this->logger->error('Form build-id mismatch detected while attempting to store a form in the cache.');
  165. return;
  166. }
  167. // Cache form structure.
  168. if (isset($form)) {
  169. if ($this->currentUser->isAuthenticated()) {
  170. $form['#cache_token'] = $this->csrfToken->get();
  171. }
  172. unset($form['#build_id_old']);
  173. $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire);
  174. }
  175. if ($data = $form_state->getCacheableArray()) {
  176. $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire);
  177. }
  178. }
  179. /**
  180. * {@inheritdoc}
  181. */
  182. public function deleteCache($form_build_id) {
  183. $this->keyValueExpirableFactory->get('form')->delete($form_build_id);
  184. $this->keyValueExpirableFactory->get('form_state')->delete($form_build_id);
  185. }
  186. }