FormValidator.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. namespace Drupal\Core\Form;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Component\Utility\Unicode;
  5. use Drupal\Core\Access\CsrfTokenGenerator;
  6. use Drupal\Core\Render\Element;
  7. use Drupal\Core\StringTranslation\StringTranslationTrait;
  8. use Drupal\Core\StringTranslation\TranslationInterface;
  9. use Psr\Log\LoggerInterface;
  10. use Symfony\Component\HttpFoundation\RequestStack;
  11. /**
  12. * Provides validation of form submissions.
  13. */
  14. class FormValidator implements FormValidatorInterface {
  15. use StringTranslationTrait;
  16. /**
  17. * The CSRF token generator to validate the form token.
  18. *
  19. * @var \Drupal\Core\Access\CsrfTokenGenerator
  20. */
  21. protected $csrfToken;
  22. /**
  23. * The request stack.
  24. *
  25. * @var \Symfony\Component\HttpFoundation\RequestStack
  26. */
  27. protected $requestStack;
  28. /**
  29. * A logger instance.
  30. *
  31. * @var \Psr\Log\LoggerInterface
  32. */
  33. protected $logger;
  34. /**
  35. * The form error handler.
  36. *
  37. * @var \Drupal\Core\Form\FormErrorHandlerInterface
  38. */
  39. protected $formErrorHandler;
  40. /**
  41. * Constructs a new FormValidator.
  42. *
  43. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  44. * The request stack.
  45. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
  46. * The string translation service.
  47. * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
  48. * The CSRF token generator.
  49. * @param \Psr\Log\LoggerInterface $logger
  50. * A logger instance.
  51. * @param \Drupal\Core\Form\FormErrorHandlerInterface $form_error_handler
  52. * The form error handler.
  53. */
  54. public function __construct(RequestStack $request_stack, TranslationInterface $string_translation, CsrfTokenGenerator $csrf_token, LoggerInterface $logger, FormErrorHandlerInterface $form_error_handler) {
  55. $this->requestStack = $request_stack;
  56. $this->stringTranslation = $string_translation;
  57. $this->csrfToken = $csrf_token;
  58. $this->logger = $logger;
  59. $this->formErrorHandler = $form_error_handler;
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public function executeValidateHandlers(&$form, FormStateInterface &$form_state) {
  65. // If there was a button pressed, use its handlers.
  66. $handlers = $form_state->getValidateHandlers();
  67. // Otherwise, check for a form-level handler.
  68. if (!$handlers && isset($form['#validate'])) {
  69. $handlers = $form['#validate'];
  70. }
  71. foreach ($handlers as $callback) {
  72. call_user_func_array($form_state->prepareCallback($callback), [&$form, &$form_state]);
  73. }
  74. }
  75. /**
  76. * {@inheritdoc}
  77. */
  78. public function validateForm($form_id, &$form, FormStateInterface &$form_state) {
  79. // If this form is flagged to always validate, ensure that previous runs of
  80. // validation are ignored.
  81. if ($form_state->isValidationEnforced()) {
  82. $form_state->setValidationComplete(FALSE);
  83. }
  84. // If this form has completed validation, do not validate again.
  85. if ($form_state->isValidationComplete()) {
  86. return;
  87. }
  88. // If the session token was set by self::prepareForm(), ensure that it
  89. // matches the current user's session. This is duplicate to code in
  90. // FormBuilder::doBuildForm() but left to protect any custom form handling
  91. // code.
  92. if (isset($form['#token'])) {
  93. if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token']) || $form_state->hasInvalidToken()) {
  94. $this->setInvalidTokenError($form_state);
  95. // Stop here and don't run any further validation handlers, because they
  96. // could invoke non-safe operations which opens the door for CSRF
  97. // vulnerabilities.
  98. $this->finalizeValidation($form, $form_state, $form_id);
  99. return;
  100. }
  101. }
  102. // Recursively validate each form element.
  103. $this->doValidateForm($form, $form_state, $form_id);
  104. $this->finalizeValidation($form, $form_state, $form_id);
  105. $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id);
  106. }
  107. /**
  108. * {@inheritdoc}
  109. */
  110. public function setInvalidTokenError(FormStateInterface $form_state) {
  111. $url = $this->requestStack->getCurrentRequest()->getRequestUri();
  112. // Setting this error will cause the form to fail validation.
  113. $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href=":link">reload this page</a>.', [':link' => $url]));
  114. }
  115. /**
  116. * Handles validation errors for forms with limited validation.
  117. *
  118. * If validation errors are limited then remove any non validated form values,
  119. * so that only values that passed validation are left for submit callbacks.
  120. *
  121. * @param array $form
  122. * An associative array containing the structure of the form.
  123. * @param \Drupal\Core\Form\FormStateInterface $form_state
  124. * The current state of the form.
  125. * @param string $form_id
  126. * The unique string identifying the form.
  127. */
  128. protected function handleErrorsWithLimitedValidation(&$form, FormStateInterface &$form_state, $form_id) {
  129. // If validation errors are limited then remove any non validated form values,
  130. // so that only values that passed validation are left for submit callbacks.
  131. $triggering_element = $form_state->getTriggeringElement();
  132. if (isset($triggering_element['#limit_validation_errors']) && $triggering_element['#limit_validation_errors'] !== FALSE) {
  133. $values = [];
  134. foreach ($triggering_element['#limit_validation_errors'] as $section) {
  135. // If the section exists within $form_state->getValues(), even if the
  136. // value is NULL, copy it to $values.
  137. $section_exists = NULL;
  138. $value = NestedArray::getValue($form_state->getValues(), $section, $section_exists);
  139. if ($section_exists) {
  140. NestedArray::setValue($values, $section, $value);
  141. }
  142. }
  143. // A button's #value does not require validation, so for convenience we
  144. // allow the value of the clicked button to be retained in its normal
  145. // $form_state->getValues() locations, even if these locations are not
  146. // included in #limit_validation_errors.
  147. if (!empty($triggering_element['#is_button'])) {
  148. $button_value = $triggering_element['#value'];
  149. // Like all input controls, the button value may be in the location
  150. // dictated by #parents. If it is, copy it to $values, but do not
  151. // override what may already be in $values.
  152. $parents = $triggering_element['#parents'];
  153. if (!NestedArray::keyExists($values, $parents) && NestedArray::getValue($form_state->getValues(), $parents) === $button_value) {
  154. NestedArray::setValue($values, $parents, $button_value);
  155. }
  156. // Additionally, self::doBuildForm() places the button value in
  157. // $form_state->getValue(BUTTON_NAME). If it's still there, after
  158. // validation handlers have run, copy it to $values, but do not override
  159. // what may already be in $values.
  160. $name = $triggering_element['#name'];
  161. if (!isset($values[$name]) && $form_state->getValue($name) === $button_value) {
  162. $values[$name] = $button_value;
  163. }
  164. }
  165. $form_state->setValues($values);
  166. }
  167. }
  168. /**
  169. * Finalizes validation.
  170. *
  171. * @param array $form
  172. * An associative array containing the structure of the form.
  173. * @param \Drupal\Core\Form\FormStateInterface $form_state
  174. * The current state of the form.
  175. * @param string $form_id
  176. * The unique string identifying the form.
  177. */
  178. protected function finalizeValidation(&$form, FormStateInterface &$form_state, $form_id) {
  179. // Delegate handling of form errors to a service.
  180. $this->formErrorHandler->handleFormErrors($form, $form_state);
  181. // Mark this form as validated.
  182. $form_state->setValidationComplete();
  183. }
  184. /**
  185. * Performs validation on form elements.
  186. *
  187. * First ensures required fields are completed, #maxlength is not exceeded,
  188. * and selected options were in the list of options given to the user. Then
  189. * calls user-defined validators.
  190. *
  191. * @param $elements
  192. * An associative array containing the structure of the form.
  193. * @param \Drupal\Core\Form\FormStateInterface $form_state
  194. * The current state of the form. The current user-submitted data is stored
  195. * in $form_state->getValues(), though form validation functions are passed
  196. * an explicit copy of the values for the sake of simplicity. Validation
  197. * handlers can also $form_state to pass information on to submit handlers.
  198. * For example:
  199. * $form_state->set('data_for_submission', $data);
  200. * This technique is useful when validation requires file parsing,
  201. * web service requests, or other expensive requests that should
  202. * not be repeated in the submission step.
  203. * @param $form_id
  204. * A unique string identifying the form for validation, submission,
  205. * theming, and hook_form_alter functions.
  206. */
  207. protected function doValidateForm(&$elements, FormStateInterface &$form_state, $form_id = NULL) {
  208. // Recurse through all children, sorting the elements so that the order of
  209. // error messages displayed to the user matches the order of elements in
  210. // the form. Use a copy of $elements so that it is not modified by the
  211. // sorting itself.
  212. $elements_sorted = $elements;
  213. foreach (Element::children($elements_sorted, TRUE) as $key) {
  214. if (isset($elements[$key]) && $elements[$key]) {
  215. $this->doValidateForm($elements[$key], $form_state);
  216. }
  217. }
  218. // Validate the current input.
  219. if (!isset($elements['#validated']) || !$elements['#validated']) {
  220. // The following errors are always shown.
  221. if (isset($elements['#needs_validation'])) {
  222. $this->performRequiredValidation($elements, $form_state);
  223. }
  224. // Set up the limited validation for errors.
  225. $form_state->setLimitValidationErrors($this->determineLimitValidationErrors($form_state));
  226. // Make sure a value is passed when the field is required.
  227. if (isset($elements['#needs_validation']) && $elements['#required']) {
  228. // A simple call to empty() will not cut it here as some fields, like
  229. // checkboxes, can return a valid value of '0'. Instead, check the
  230. // length if it's a string, and the item count if it's an array.
  231. // An unchecked checkbox has a #value of integer 0, different than
  232. // string '0', which could be a valid value.
  233. $is_countable = is_array($elements['#value']) || $elements['#value'] instanceof \Countable;
  234. $is_empty_multiple = $is_countable && count($elements['#value']) == 0;
  235. $is_empty_string = (is_string($elements['#value']) && Unicode::strlen(trim($elements['#value'])) == 0);
  236. $is_empty_value = ($elements['#value'] === 0);
  237. $is_empty_null = is_null($elements['#value']);
  238. if ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null) {
  239. // Flag this element as #required_but_empty to allow #element_validate
  240. // handlers to set a custom required error message, but without having
  241. // to re-implement the complex logic to figure out whether the field
  242. // value is empty.
  243. $elements['#required_but_empty'] = TRUE;
  244. }
  245. }
  246. // Call user-defined form level validators.
  247. if (isset($form_id)) {
  248. $this->executeValidateHandlers($elements, $form_state);
  249. }
  250. // Call any element-specific validators. These must act on the element
  251. // #value data.
  252. elseif (isset($elements['#element_validate'])) {
  253. foreach ($elements['#element_validate'] as $callback) {
  254. $complete_form = &$form_state->getCompleteForm();
  255. call_user_func_array($form_state->prepareCallback($callback), [&$elements, &$form_state, &$complete_form]);
  256. }
  257. }
  258. // Ensure that a #required form error is thrown, regardless of whether
  259. // #element_validate handlers changed any properties. If $is_empty_value
  260. // is defined, then above #required validation code ran, so the other
  261. // variables are also known to be defined and we can test them again.
  262. if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null)) {
  263. if (isset($elements['#required_error'])) {
  264. $form_state->setError($elements, $elements['#required_error']);
  265. }
  266. // A #title is not mandatory for form elements, but without it we cannot
  267. // set a form error message. So when a visible title is undesirable,
  268. // form constructors are encouraged to set #title anyway, and then set
  269. // #title_display to 'invisible'. This improves accessibility.
  270. elseif (isset($elements['#title'])) {
  271. $form_state->setError($elements, $this->t('@name field is required.', ['@name' => $elements['#title']]));
  272. }
  273. else {
  274. $form_state->setError($elements);
  275. }
  276. }
  277. $elements['#validated'] = TRUE;
  278. }
  279. // Done validating this element, so turn off error suppression.
  280. // self::doValidateForm() turns it on again when starting on the next
  281. // element, if it's still appropriate to do so.
  282. $form_state->setLimitValidationErrors(NULL);
  283. }
  284. /**
  285. * Performs validation of elements that are not subject to limited validation.
  286. *
  287. * @param array $elements
  288. * An associative array containing the structure of the form.
  289. * @param \Drupal\Core\Form\FormStateInterface $form_state
  290. * The current state of the form. The current user-submitted data is stored
  291. * in $form_state->getValues(), though form validation functions are passed
  292. * an explicit copy of the values for the sake of simplicity. Validation
  293. * handlers can also $form_state to pass information on to submit handlers.
  294. * For example:
  295. * $form_state->set('data_for_submission', $data);
  296. * This technique is useful when validation requires file parsing,
  297. * web service requests, or other expensive requests that should
  298. * not be repeated in the submission step.
  299. */
  300. protected function performRequiredValidation(&$elements, FormStateInterface &$form_state) {
  301. // Verify that the value is not longer than #maxlength.
  302. if (isset($elements['#maxlength']) && Unicode::strlen($elements['#value']) > $elements['#maxlength']) {
  303. $form_state->setError($elements, $this->t('@name cannot be longer than %max characters but is currently %length characters long.', ['@name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => Unicode::strlen($elements['#value'])]));
  304. }
  305. if (isset($elements['#options']) && isset($elements['#value'])) {
  306. if ($elements['#type'] == 'select') {
  307. $options = OptGroup::flattenOptions($elements['#options']);
  308. }
  309. else {
  310. $options = $elements['#options'];
  311. }
  312. if (is_array($elements['#value'])) {
  313. $value = in_array($elements['#type'], ['checkboxes', 'tableselect']) ? array_keys($elements['#value']) : $elements['#value'];
  314. foreach ($value as $v) {
  315. if (!isset($options[$v])) {
  316. $form_state->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
  317. $this->logger->error('Illegal choice %choice in %name element.', ['%choice' => $v, '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']]);
  318. }
  319. }
  320. }
  321. // Non-multiple select fields always have a value in HTML. If the user
  322. // does not change the form, it will be the value of the first option.
  323. // Because of this, form validation for the field will almost always
  324. // pass, even if the user did not select anything. To work around this
  325. // browser behavior, required select fields without a #default_value
  326. // get an additional, first empty option. In case the submitted value
  327. // is identical to the empty option's value, we reset the element's
  328. // value to NULL to trigger the regular #required handling below.
  329. // @see \Drupal\Core\Render\Element\Select::processSelect()
  330. elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
  331. $elements['#value'] = NULL;
  332. $form_state->setValueForElement($elements, NULL);
  333. }
  334. elseif (!isset($options[$elements['#value']])) {
  335. $form_state->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
  336. $this->logger->error('Illegal choice %choice in %name element.', ['%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']]);
  337. }
  338. }
  339. }
  340. /**
  341. * Determines if validation errors should be limited.
  342. *
  343. * @param \Drupal\Core\Form\FormStateInterface $form_state
  344. * The current state of the form.
  345. *
  346. * @return array|null
  347. */
  348. protected function determineLimitValidationErrors(FormStateInterface &$form_state) {
  349. // While this element is being validated, it may be desired that some
  350. // calls to \Drupal\Core\Form\FormStateInterface::setErrorByName() be
  351. // suppressed and not result in a form error, so that a button that
  352. // implements low-risk functionality (such as "Previous" or "Add more") that
  353. // doesn't require all user input to be valid can still have its submit
  354. // handlers triggered. The triggering element's #limit_validation_errors
  355. // property contains the information for which errors are needed, and all
  356. // other errors are to be suppressed. The #limit_validation_errors property
  357. // is ignored if submit handlers will run, but the element doesn't have a
  358. // #submit property, because it's too large a security risk to have any
  359. // invalid user input when executing form-level submit handlers.
  360. $triggering_element = $form_state->getTriggeringElement();
  361. if (isset($triggering_element['#limit_validation_errors']) && ($triggering_element['#limit_validation_errors'] !== FALSE) && !($form_state->isSubmitted() && !isset($triggering_element['#submit']))) {
  362. return $triggering_element['#limit_validation_errors'];
  363. }
  364. // If submit handlers won't run (due to the submission having been
  365. // triggered by an element whose #executes_submit_callback property isn't
  366. // TRUE), then it's safe to suppress all validation errors, and we do so
  367. // by default, which is particularly useful during an Ajax submission
  368. // triggered by a non-button. An element can override this default by
  369. // setting the #limit_validation_errors property. For button element
  370. // types, #limit_validation_errors defaults to FALSE, so that full
  371. // validation is their default behavior.
  372. elseif ($triggering_element && !isset($triggering_element['#limit_validation_errors']) && !$form_state->isSubmitted()) {
  373. return [];
  374. }
  375. // As an extra security measure, explicitly turn off error suppression if
  376. // one of the above conditions wasn't met. Since this is also done at the
  377. // end of this function, doing it here is only to handle the rare edge
  378. // case where a validate handler invokes form processing of another form.
  379. else {
  380. return NULL;
  381. }
  382. }
  383. }