FormValidator.php 19 KB
Newer Older
1
2
3
4
5
6
7
8
9
<?php

namespace Drupal\Core\Form;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
10
use Psr\Log\LoggerInterface;
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Provides validation of form submissions.
 */
class FormValidator implements FormValidatorInterface {

  use StringTranslationTrait;

  /**
   * The CSRF token generator to validate the form token.
   *
   * @var \Drupal\Core\Access\CsrfTokenGenerator
   */
  protected $csrfToken;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

34
35
36
37
38
39
40
  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

41
42
43
44
45
46
47
  /**
   * The form error handler.
   *
   * @var \Drupal\Core\Form\FormErrorHandlerInterface
   */
  protected $formErrorHandler;

48
49
50
51
52
53
54
55
56
  /**
   * Constructs a new FormValidator.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The string translation service.
   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
   *   The CSRF token generator.
57
58
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
59
60
   * @param \Drupal\Core\Form\FormErrorHandlerInterface $form_error_handler
   *   The form error handler.
61
   */
62
  public function __construct(RequestStack $request_stack, TranslationInterface $string_translation, CsrfTokenGenerator $csrf_token, LoggerInterface $logger, FormErrorHandlerInterface $form_error_handler) {
63
64
65
    $this->requestStack = $request_stack;
    $this->stringTranslation = $string_translation;
    $this->csrfToken = $csrf_token;
66
    $this->logger = $logger;
67
    $this->formErrorHandler = $form_error_handler;
68
69
70
71
72
  }

  /**
   * {@inheritdoc}
   */
73
  public function executeValidateHandlers(&$form, FormStateInterface &$form_state) {
74
    // If there was a button pressed, use its handlers.
75
    $handlers = $form_state->getValidateHandlers();
76
    // Otherwise, check for a form-level handler.
77
    if (!$handlers && isset($form['#validate'])) {
78
79
80
      $handlers = $form['#validate'];
    }

81
    foreach ($handlers as $callback) {
82
      call_user_func_array($form_state->prepareCallback($callback), [&$form, &$form_state]);
83
84
85
86
87
88
    }
  }

  /**
   * {@inheritdoc}
   */
89
  public function validateForm($form_id, &$form, FormStateInterface &$form_state) {
90
91
    // If this form is flagged to always validate, ensure that previous runs of
    // validation are ignored.
92
93
    if ($form_state->isValidationEnforced()) {
      $form_state->setValidationComplete(FALSE);
94
95
96
    }

    // If this form has completed validation, do not validate again.
97
    if ($form_state->isValidationComplete()) {
98
99
100
101
      return;
    }

    // If the session token was set by self::prepareForm(), ensure that it
102
103
104
    // matches the current user's session. This is duplicate to code in
    // FormBuilder::doBuildForm() but left to protect any custom form handling
    // code.
105
    if (isset($form['#token'])) {
106
107
      if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token']) || $form_state->hasInvalidToken()) {
        $this->setInvalidTokenError($form_state);
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

        // Stop here and don't run any further validation handlers, because they
        // could invoke non-safe operations which opens the door for CSRF
        // vulnerabilities.
        $this->finalizeValidation($form, $form_state, $form_id);
        return;
      }
    }

    // Recursively validate each form element.
    $this->doValidateForm($form, $form_state, $form_id);
    $this->finalizeValidation($form, $form_state, $form_id);
    $this->handleErrorsWithLimitedValidation($form, $form_state, $form_id);
  }

123
124
125
126
127
  /**
   * {@inheritdoc}
   */
  public function setInvalidTokenError(FormStateInterface $form_state) {
    // Setting this error will cause the form to fail validation.
128
    $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));
129
130
  }

131
132
133
134
135
136
137
138
  /**
   * Handles validation errors for forms with limited validation.
   *
   * If validation errors are limited then remove any non validated form values,
   * so that only values that passed validation are left for submit callbacks.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
139
140
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
141
142
143
   * @param string $form_id
   *   The unique string identifying the form.
   */
144
  protected function handleErrorsWithLimitedValidation(&$form, FormStateInterface &$form_state, $form_id) {
145
146
    // If validation errors are limited then remove any non validated form values,
    // so that only values that passed validation are left for submit callbacks.
147
148
    $triggering_element = $form_state->getTriggeringElement();
    if (isset($triggering_element['#limit_validation_errors']) && $triggering_element['#limit_validation_errors'] !== FALSE) {
149
      $values = [];
150
      foreach ($triggering_element['#limit_validation_errors'] as $section) {
151
152
        // If the section exists within $form_state->getValues(), even if the
        // value is NULL, copy it to $values.
153
        $section_exists = NULL;
154
        $value = NestedArray::getValue($form_state->getValues(), $section, $section_exists);
155
156
157
158
159
160
        if ($section_exists) {
          NestedArray::setValue($values, $section, $value);
        }
      }
      // A button's #value does not require validation, so for convenience we
      // allow the value of the clicked button to be retained in its normal
161
      // $form_state->getValues() locations, even if these locations are not
162
      // included in #limit_validation_errors.
163
164
      if (!empty($triggering_element['#is_button'])) {
        $button_value = $triggering_element['#value'];
165
166
167
168

        // Like all input controls, the button value may be in the location
        // dictated by #parents. If it is, copy it to $values, but do not
        // override what may already be in $values.
169
        $parents = $triggering_element['#parents'];
170
        if (!NestedArray::keyExists($values, $parents) && NestedArray::getValue($form_state->getValues(), $parents) === $button_value) {
171
172
173
174
          NestedArray::setValue($values, $parents, $button_value);
        }

        // Additionally, self::doBuildForm() places the button value in
175
        // $form_state->getValue(BUTTON_NAME). If it's still there, after
176
177
        // validation handlers have run, copy it to $values, but do not override
        // what may already be in $values.
178
        $name = $triggering_element['#name'];
179
        if (!isset($values[$name]) && $form_state->getValue($name) === $button_value) {
180
181
182
          $values[$name] = $button_value;
        }
      }
183
      $form_state->setValues($values);
184
185
186
187
188
189
190
191
    }
  }

  /**
   * Finalizes validation.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
192
193
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
194
195
196
   * @param string $form_id
   *   The unique string identifying the form.
   */
197
  protected function finalizeValidation(&$form, FormStateInterface &$form_state, $form_id) {
198
199
200
    // Delegate handling of form errors to a service.
    $this->formErrorHandler->handleFormErrors($form, $form_state);

201
    // Mark this form as validated.
202
    $form_state->setValidationComplete();
203
204
205
206
207
208
209
210
211
212
213
  }

  /**
   * Performs validation on form elements.
   *
   * First ensures required fields are completed, #maxlength is not exceeded,
   * and selected options were in the list of options given to the user. Then
   * calls user-defined validators.
   *
   * @param $elements
   *   An associative array containing the structure of the form.
214
215
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form. The current user-submitted data is stored
216
217
   *   in $form_state->getValues(), though form validation functions are passed
   *   an explicit copy of the values for the sake of simplicity. Validation
218
219
   *   handlers can also $form_state to pass information on to submit handlers.
   *   For example:
220
   *     $form_state->set('data_for_submission', $data);
221
222
223
224
225
   *   This technique is useful when validation requires file parsing,
   *   web service requests, or other expensive requests that should
   *   not be repeated in the submission step.
   * @param $form_id
   *   A unique string identifying the form for validation, submission,
226
227
228
   *   theming, and hook_form_alter functions. Is only present on the initial
   *   call to the method, which receives the entire form array as the $element,
   *   and not on recursive calls.
229
   */
230
  protected function doValidateForm(&$elements, FormStateInterface &$form_state, $form_id = NULL) {
231
232
233
234
235
236
    // Recurse through all children, sorting the elements so that the order of
    // error messages displayed to the user matches the order of elements in
    // the form. Use a copy of $elements so that it is not modified by the
    // sorting itself.
    $elements_sorted = $elements;
    foreach (Element::children($elements_sorted, TRUE) as $key) {
237
238
239
240
241
242
243
244
245
246
247
248
249
      if (isset($elements[$key]) && $elements[$key]) {
        $this->doValidateForm($elements[$key], $form_state);
      }
    }

    // Validate the current input.
    if (!isset($elements['#validated']) || !$elements['#validated']) {
      // The following errors are always shown.
      if (isset($elements['#needs_validation'])) {
        $this->performRequiredValidation($elements, $form_state);
      }

      // Set up the limited validation for errors.
250
      $form_state->setLimitValidationErrors($this->determineLimitValidationErrors($form_state));
251
252
253
254
255
256
257
258

      // Make sure a value is passed when the field is required.
      if (isset($elements['#needs_validation']) && $elements['#required']) {
        // A simple call to empty() will not cut it here as some fields, like
        // checkboxes, can return a valid value of '0'. Instead, check the
        // length if it's a string, and the item count if it's an array.
        // An unchecked checkbox has a #value of integer 0, different than
        // string '0', which could be a valid value.
259
260
        $is_countable = is_array($elements['#value']) || $elements['#value'] instanceof \Countable;
        $is_empty_multiple = $is_countable && count($elements['#value']) == 0;
261
        $is_empty_string = (is_string($elements['#value']) && mb_strlen(trim($elements['#value'])) == 0);
262
        $is_empty_value = ($elements['#value'] === 0);
263
264
        $is_empty_null = is_null($elements['#value']);
        if ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null) {
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
          // Flag this element as #required_but_empty to allow #element_validate
          // handlers to set a custom required error message, but without having
          // to re-implement the complex logic to figure out whether the field
          // value is empty.
          $elements['#required_but_empty'] = TRUE;
        }
      }

      // Call user-defined form level validators.
      if (isset($form_id)) {
        $this->executeValidateHandlers($elements, $form_state);
      }
      // Call any element-specific validators. These must act on the element
      // #value data.
      elseif (isset($elements['#element_validate'])) {
        foreach ($elements['#element_validate'] as $callback) {
281
          $complete_form = &$form_state->getCompleteForm();
282
          call_user_func_array($form_state->prepareCallback($callback), [&$elements, &$form_state, &$complete_form]);
283
284
285
286
287
288
289
        }
      }

      // Ensure that a #required form error is thrown, regardless of whether
      // #element_validate handlers changed any properties. If $is_empty_value
      // is defined, then above #required validation code ran, so the other
      // variables are also known to be defined and we can test them again.
290
      if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null)) {
291
        if (isset($elements['#required_error'])) {
292
          $form_state->setError($elements, $elements['#required_error']);
293
294
295
296
297
298
        }
        // A #title is not mandatory for form elements, but without it we cannot
        // set a form error message. So when a visible title is undesirable,
        // form constructors are encouraged to set #title anyway, and then set
        // #title_display to 'invisible'. This improves accessibility.
        elseif (isset($elements['#title'])) {
299
          $form_state->setError($elements, $this->t('@name field is required.', ['@name' => $elements['#title']]));
300
301
        }
        else {
302
          $form_state->setError($elements);
303
304
305
306
307
308
309
310
311
        }
      }

      $elements['#validated'] = TRUE;
    }

    // Done validating this element, so turn off error suppression.
    // self::doValidateForm() turns it on again when starting on the next
    // element, if it's still appropriate to do so.
312
    $form_state->setLimitValidationErrors(NULL);
313
314
315
316
317
318
319
  }

  /**
   * Performs validation of elements that are not subject to limited validation.
   *
   * @param array $elements
   *   An associative array containing the structure of the form.
320
321
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form. The current user-submitted data is stored
322
323
   *   in $form_state->getValues(), though form validation functions are passed
   *   an explicit copy of the values for the sake of simplicity. Validation
324
325
   *   handlers can also $form_state to pass information on to submit handlers.
   *   For example:
326
   *     $form_state->set('data_for_submission', $data);
327
328
329
330
   *   This technique is useful when validation requires file parsing,
   *   web service requests, or other expensive requests that should
   *   not be repeated in the submission step.
   */
331
  protected function performRequiredValidation(&$elements, FormStateInterface &$form_state) {
332
    // Verify that the value is not longer than #maxlength.
333
334
    if (isset($elements['#maxlength']) && mb_strlen($elements['#value']) > $elements['#maxlength']) {
      $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' => mb_strlen($elements['#value'])]));
335
336
337
338
339
340
341
342
343
344
    }

    if (isset($elements['#options']) && isset($elements['#value'])) {
      if ($elements['#type'] == 'select') {
        $options = OptGroup::flattenOptions($elements['#options']);
      }
      else {
        $options = $elements['#options'];
      }
      if (is_array($elements['#value'])) {
345
        $value = in_array($elements['#type'], ['checkboxes', 'tableselect']) ? array_keys($elements['#value']) : $elements['#value'];
346
347
        foreach ($value as $v) {
          if (!isset($options[$v])) {
348
            $form_state->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
349
            $this->logger->error('Illegal choice %choice in %name element.', ['%choice' => $v, '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']]);
350
351
352
353
354
355
356
357
358
359
360
          }
        }
      }
      // Non-multiple select fields always have a value in HTML. If the user
      // does not change the form, it will be the value of the first option.
      // Because of this, form validation for the field will almost always
      // pass, even if the user did not select anything. To work around this
      // browser behavior, required select fields without a #default_value
      // get an additional, first empty option. In case the submitted value
      // is identical to the empty option's value, we reset the element's
      // value to NULL to trigger the regular #required handling below.
361
      // @see \Drupal\Core\Render\Element\Select::processSelect()
362
363
      elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
        $elements['#value'] = NULL;
364
        $form_state->setValueForElement($elements, NULL);
365
366
      }
      elseif (!isset($options[$elements['#value']])) {
367
        $form_state->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
368
        $this->logger->error('Illegal choice %choice in %name element.', ['%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']]);
369
370
371
372
373
374
375
      }
    }
  }

  /**
   * Determines if validation errors should be limited.
   *
376
377
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
378
379
380
   *
   * @return array|null
   */
381
  protected function determineLimitValidationErrors(FormStateInterface &$form_state) {
382
    // While this element is being validated, it may be desired that some
383
384
385
386
387
388
389
390
391
392
    // calls to \Drupal\Core\Form\FormStateInterface::setErrorByName() be
    // suppressed and not result in a form error, so that a button that
    // implements low-risk functionality (such as "Previous" or "Add more") that
    // doesn't require all user input to be valid can still have its submit
    // handlers triggered. The triggering element's #limit_validation_errors
    // property contains the information for which errors are needed, and all
    // other errors are to be suppressed. The #limit_validation_errors property
    // is ignored if submit handlers will run, but the element doesn't have a
    // #submit property, because it's too large a security risk to have any
    // invalid user input when executing form-level submit handlers.
393
394
395
    $triggering_element = $form_state->getTriggeringElement();
    if (isset($triggering_element['#limit_validation_errors']) && ($triggering_element['#limit_validation_errors'] !== FALSE) && !($form_state->isSubmitted() && !isset($triggering_element['#submit']))) {
      return $triggering_element['#limit_validation_errors'];
396
397
398
399
400
401
402
    }
    // If submit handlers won't run (due to the submission having been
    // triggered by an element whose #executes_submit_callback property isn't
    // TRUE), then it's safe to suppress all validation errors, and we do so
    // by default, which is particularly useful during an Ajax submission
    // triggered by a non-button. An element can override this default by
    // setting the #limit_validation_errors property. For button element
403
404
    // types, #limit_validation_errors defaults to FALSE, so that full
    // validation is their default behavior.
405
    elseif ($triggering_element && !isset($triggering_element['#limit_validation_errors']) && !$form_state->isSubmitted()) {
406
      return [];
407
408
409
410
411
412
413
414
415
416
417
    }
    // As an extra security measure, explicitly turn off error suppression if
    // one of the above conditions wasn't met. Since this is also done at the
    // end of this function, doing it here is only to handle the rare edge
    // case where a validate handler invokes form processing of another form.
    else {
      return NULL;
    }
  }

}