"Fossies" - the Fresh Open Source Software Archive

Member "drupal-8.9.10/core/lib/Drupal/Core/Form/FormValidator.php" (26 Nov 2020, 19265 Bytes) of package /linux/www/drupal-8.9.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "FormValidator.php" see the Fossies "Dox" file reference documentation.

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