Issue #2209977 by tim.plunkett: Move form validation logic out of FormBuilder into a new class.

8.0.x
Alex Pott 2014-05-06 00:07:47 +01:00
parent 02eb3d3fc9
commit f710a6c92e
14 changed files with 1359 additions and 783 deletions

View File

@ -126,7 +126,10 @@ services:
arguments: [default]
form_builder:
class: Drupal\Core\Form\FormBuilder
arguments: ['@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@url_generator', '@string_translation', '@request_stack', '@?csrf_token', '@?http_kernel']
arguments: ['@form_validator', '@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@url_generator', '@request_stack', '@?csrf_token', '@?http_kernel']
form_validator:
class: Drupal\Core\Form\FormValidator
arguments: ['@request_stack', '@string_translation', '@csrf_token']
keyvalue:
class: Drupal\Core\KeyValueStore\KeyValueFactory
arguments: ['@service_container', '@settings']

View File

@ -11,6 +11,7 @@ use Drupal\Component\Utility\String;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
@ -323,7 +324,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->validateForm().
*
* @see \Drupal\Core\Form\FormBuilderInterface::validateForm().
* @see \Drupal\Core\Form\FormValidatorInterface::validateForm().
*/
function drupal_validate_form($form_id, &$form, &$form_state) {
\Drupal::formBuilder()->validateForm($form_id, $form, $form_state);
@ -345,12 +346,19 @@ function drupal_redirect_form($form_state) {
* Executes custom validation and submission handlers for a given form.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->executeHandlers().
* Use either \Drupal::formBuilder()->executeSubmitHandlers() or
* \Drupal::service('form_validator')->executeValidateHandlers().
*
* @see \Drupal\Core\Form\FormBuilderInterface::executeHandlers().
* @see \Drupal\Core\Form\FormBuilderInterface::executeSubmitHandlers()
* @see \Drupal\Core\Form\FormValidatorInterface::executeValidateHandlers()
*/
function form_execute_handlers($type, &$form, &$form_state) {
\Drupal::formBuilder()->executeHandlers($type, $form, $form_state);
if ($type == 'submit') {
\Drupal::formBuilder()->executeSubmitHandlers($form, $form_state);
}
elseif ($type == 'validate') {
\Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
}
}
/**
@ -359,7 +367,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->setErrorByName().
*
* @see \Drupal\Core\Form\FormBuilderInterface::setErrorByName().
* @see \Drupal\Core\Form\FormErrorInterface::setErrorByName().
*/
function form_set_error($name, array &$form_state, $message = '') {
\Drupal::formBuilder()->setErrorByName($name, $form_state, $message);
@ -383,7 +391,7 @@ function form_clear_error(array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->getErrors().
*
* @see \Drupal\Core\Form\FormBuilderInterface::getErrors().
* @see \Drupal\Core\Form\FormErrorInterface::getErrors()
*/
function form_get_errors(array &$form_state) {
return \Drupal::formBuilder()->getErrors($form_state);
@ -395,7 +403,7 @@ function form_get_errors(array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->getError().
*
* @see \Drupal\Core\Form\FormBuilderInterface::getError().
* @see \Drupal\Core\Form\FormErrorInterface::getError().
*/
function form_get_error($element, array &$form_state) {
return \Drupal::formBuilder()->getError($element, $form_state);
@ -407,7 +415,7 @@ function form_get_error($element, array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->setError().
*
* @see \Drupal\Core\Form\FormBuilderInterface::setError().
* @see \Drupal\Core\Form\FormErrorInterface::setError().
*/
function form_error(&$element, array &$form_state, $message = '') {
\Drupal::formBuilder()->setError($element, $form_state, $message);
@ -844,12 +852,10 @@ function form_set_value($element, $value, &$form_state) {
* An array with all hierarchical elements flattened to a single array.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->flattenOptions().
*
* @see \Drupal\Core\Form\FormBuilderInterface::flattenOptions().
* Use \Drupal\Core\Form\OptGroup::flattenOptions().
*/
function form_options_flatten($array) {
return \Drupal::formBuilder()->flattenOptions($array);
return OptGroup::flattenOptions($array);
}
/**

View File

@ -9,7 +9,6 @@ namespace Drupal\Core\Form;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Extension\ModuleHandlerInterface;
@ -18,12 +17,9 @@ use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@ -33,8 +29,7 @@ use Symfony\Component\HttpKernel\KernelEvents;
/**
* Provides form building and processing.
*/
class FormBuilder implements FormBuilderInterface {
use StringTranslationTrait;
class FormBuilder implements FormBuilderInterface, FormValidatorInterface {
/**
* The module handler.
@ -46,7 +41,7 @@ class FormBuilder implements FormBuilderInterface {
/**
* The factory for expirable key value stores used by form cache.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
* @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
*/
protected $keyValueExpirableFactory;
@ -93,24 +88,15 @@ class FormBuilder implements FormBuilderInterface {
protected $currentUser;
/**
* An array of known forms.
*
* @see self::retrieveForms()
*
* @var array
* @var \Drupal\Core\Form\FormValidatorInterface
*/
protected $forms;
/**
* An array of options used for recursive flattening.
*
* @var array
*/
protected $flattenedOptions = array();
protected $formValidator;
/**
* Constructs a new FormBuilder.
*
* @param \Drupal\Core\Form\FormValidatorInterface $form_validator
* The form validator.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
@ -119,8 +105,6 @@ class FormBuilder implements FormBuilderInterface {
* The event dispatcher.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
@ -128,12 +112,12 @@ class FormBuilder implements FormBuilderInterface {
* @param \Drupal\Core\HttpKernel $http_kernel
* The HTTP kernel.
*/
public function __construct(ModuleHandlerInterface $module_handler, KeyValueExpirableFactoryInterface $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, UrlGeneratorInterface $url_generator, TranslationInterface $string_translation, RequestStack $request_stack, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
public function __construct(FormValidatorInterface $form_validator, ModuleHandlerInterface $module_handler, KeyValueExpirableFactoryInterface $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, UrlGeneratorInterface $url_generator, RequestStack $request_stack, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
$this->formValidator = $form_validator;
$this->moduleHandler = $module_handler;
$this->keyValueExpirableFactory = $key_value_expirable_factory;
$this->eventDispatcher = $event_dispatcher;
$this->urlGenerator = $url_generator;
$this->stringTranslation = $string_translation;
$this->requestStack = $request_stack;
$this->csrfToken = $csrf_token;
$this->httpKernel = $http_kernel;
@ -559,7 +543,7 @@ class FormBuilder implements FormBuilderInterface {
if ($form_state['programmed'] && !isset($form_state['triggering_element']) && count($form_state['buttons']) == 1) {
$form_state['triggering_element'] = reset($form_state['buttons']);
}
$this->validateForm($form_id, $form, $form_state);
$this->formValidator->validateForm($form_id, $form, $form_state);
// drupal_html_id() maintains a cache of element IDs it has seen, so it
// can prevent duplicates. We want to be sure we reset that cache when a
@ -571,9 +555,10 @@ class FormBuilder implements FormBuilderInterface {
$this->drupalStaticReset('drupal_html_id');
}
// @todo Move into a dedicated class in https://drupal.org/node/2257835.
if ($form_state['submitted'] && !$this->getAnyErrors() && !$form_state['rebuild']) {
// Execute form submit handlers.
$this->executeHandlers('submit', $form, $form_state);
$this->executeSubmitHandlers($form, $form_state);
// If batches were set in the submit handlers, we process them now,
// possibly ending execution. We make sure we do not react to the batch
@ -583,7 +568,7 @@ class FormBuilder implements FormBuilderInterface {
// Store $form_state information in the batch definition.
// We need the full $form_state when either:
// - Some submit handlers were saved to be called during batch
// processing. See self::executeHandlers().
// processing. See self::executeSubmitHandlers().
// - The form is multistep.
// In other cases, we only need the information expected by
// self::redirectForm().
@ -801,97 +786,7 @@ class FormBuilder implements FormBuilderInterface {
* {@inheritdoc}
*/
public function validateForm($form_id, &$form, &$form_state) {
// If this form is flagged to always validate, ensure that previous runs of
// validation are ignored.
if (!empty($form_state['must_validate'])) {
$form_state['validation_complete'] = FALSE;
}
// If this form has completed validation, do not validate again.
if (!empty($form_state['validation_complete'])) {
return;
}
// If the session token was set by self::prepareForm(), ensure that it
// matches the current user's session.
if (isset($form['#token'])) {
if (!$this->csrfToken->validate($form_state['values']['form_token'], $form['#token'])) {
$request = $this->requestStack->getCurrentRequest();
$path = $request->attributes->get('_system_path');
$query = UrlHelper::filterQueryParameters($request->query->all());
$url = $this->urlGenerator->generateFromPath($path, array('query' => $query));
// Setting this error will cause the form to fail validation.
$this->setErrorByName('form_token', $form_state, $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
// 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_id, $form, $form_state);
return;
}
}
// Recursively validate each form element.
$this->doValidateForm($form, $form_state, $form_id);
$this->finalizeValidation($form_id, $form, $form_state);
// If validation errors are limited then remove any non validated form values,
// so that only values that passed validation are left for submit callbacks.
if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) {
$values = array();
foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) {
// If the section exists within $form_state['values'], even if the value
// is NULL, copy it to $values.
$section_exists = NULL;
$value = NestedArray::getValue($form_state['values'], $section, $section_exists);
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
// $form_state['values'] locations, even if these locations are not
// included in #limit_validation_errors.
if (!empty($form_state['triggering_element']['#is_button'])) {
$button_value = $form_state['triggering_element']['#value'];
// 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.
$parents = $form_state['triggering_element']['#parents'];
if (!NestedArray::keyExists($values, $parents) && NestedArray::getValue($form_state['values'], $parents) === $button_value) {
NestedArray::setValue($values, $parents, $button_value);
}
// Additionally, self::doBuildForm() places the button value in
// $form_state['values'][BUTTON_NAME]. If it's still there, after
// validation handlers have run, copy it to $values, but do not override
// what may already be in $values.
$name = $form_state['triggering_element']['#name'];
if (!isset($values[$name]) && isset($form_state['values'][$name]) && $form_state['values'][$name] === $button_value) {
$values[$name] = $button_value;
}
}
$form_state['values'] = $values;
}
}
/**
* Finalizes validation.
*
* @param string $form_id
* The unique string identifying the form.
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* An associative array containing the current state of the form.
*/
protected function finalizeValidation($form_id, &$form, &$form_state) {
// After validation, loop through and assign each element its errors.
$this->setElementErrorsFromFormState($form, $form_state);
// Mark this form as validated.
$form_state['validation_complete'] = TRUE;
$this->formValidator->validateForm($form_id, $form, $form_state);
}
/**
@ -973,209 +868,23 @@ class FormBuilder implements FormBuilderInterface {
}
/**
* 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.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* 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,
* theming, and hook_form_alter functions.
* {@inheritdoc}
*/
protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
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'])) {
// Verify that the value is not longer than #maxlength.
if (isset($elements['#maxlength']) && Unicode::strlen($elements['#value']) > $elements['#maxlength']) {
$this->setError($elements, $form_state, $this->t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => Unicode::strlen($elements['#value']))));
}
if (isset($elements['#options']) && isset($elements['#value'])) {
if ($elements['#type'] == 'select') {
$options = $this->flattenOptions($elements['#options']);
}
else {
$options = $elements['#options'];
}
if (is_array($elements['#value'])) {
$value = in_array($elements['#type'], array('checkboxes', 'tableselect')) ? array_keys($elements['#value']) : $elements['#value'];
foreach ($value as $v) {
if (!isset($options[$v])) {
$this->setError($elements, $form_state, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$this->watchdog('form', 'Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
}
}
}
// 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.
// @see form_process_select()
elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
$elements['#value'] = NULL;
$this->setValue($elements, NULL, $form_state);
}
elseif (!isset($options[$elements['#value']])) {
$this->setError($elements, $form_state, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$this->watchdog('form', 'Illegal choice %choice in %name element.', array('%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
}
}
}
// While this element is being validated, it may be desired that some
// calls to self::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.
if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) {
$form_state['limit_validation_errors'] = $form_state['triggering_element']['#limit_validation_errors'];
}
// 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
// types, #limit_validation_errors defaults to FALSE (via
// system_element_info()), so that full validation is their default
// behavior.
elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) {
$form_state['limit_validation_errors'] = array();
}
// 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 {
$form_state['limit_validation_errors'] = NULL;
}
// 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.
$is_empty_multiple = (!count($elements['#value']));
$is_empty_string = (is_string($elements['#value']) && Unicode::strlen(trim($elements['#value'])) == 0);
$is_empty_value = ($elements['#value'] === 0);
if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
// 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->executeHandlers('validate', $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) {
call_user_func_array($callback, array(&$elements, &$form_state, &$form_state['complete_form']));
}
}
// 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.
if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value)) {
if (isset($elements['#required_error'])) {
$this->setError($elements, $form_state, $elements['#required_error']);
}
// 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'])) {
$this->setError($elements, $form_state, $this->t('!name field is required.', array('!name' => $elements['#title'])));
}
else {
$this->setError($elements, $form_state);
}
}
$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.
$form_state['limit_validation_errors'] = NULL;
}
/**
* Stores the errors of each element directly on the element.
*
* Because self::getError() and self::getErrors() require the $form_state,
* we must provide a way for non-form functions to check the errors for a
* specific element. The most common usage of this is a #pre_render callback.
*
* @param array $elements
* An associative array containing the structure of a form element.
* @param array $form_state
* An associative array containing the current state of the form.
*/
protected function setElementErrorsFromFormState(array &$elements, array &$form_state) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
$this->setElementErrorsFromFormState($elements[$key], $form_state);
}
}
// Store the errors for this element on the element directly.
$elements['#errors'] = $this->getError($elements, $form_state);
public function executeValidateHandlers(&$form, &$form_state) {
$this->formValidator->executeValidateHandlers($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function executeHandlers($type, &$form, &$form_state) {
public function executeSubmitHandlers(&$form, &$form_state) {
// If there was a button pressed, use its handlers.
if (isset($form_state[$type . '_handlers'])) {
$handlers = $form_state[$type . '_handlers'];
if (isset($form_state['submit_handlers'])) {
$handlers = $form_state['submit_handlers'];
}
// Otherwise, check for a form-level handler.
elseif (isset($form['#' . $type])) {
$handlers = $form['#' . $type];
elseif (isset($form['#submit'])) {
$handlers = $form['#submit'];
}
else {
$handlers = array();
@ -1185,7 +894,7 @@ class FormBuilder implements FormBuilderInterface {
// Check if a previous _submit handler has set a batch, but make sure we
// do not react to a batch that is already being processed (for instance
// if a batch operation performs a self::submitForm()).
if ($type == 'submit' && ($batch = &$this->batchGet()) && !isset($batch['id'])) {
if (($batch = &$this->batchGet()) && !isset($batch['id'])) {
// Some previous submit handler has set a batch. To ensure correct
// execution order, store the call in a special 'control' batch set.
// See _batch_next_set().
@ -1202,93 +911,42 @@ class FormBuilder implements FormBuilderInterface {
* {@inheritdoc}
*/
public function setErrorByName($name, array &$form_state, $message = '') {
if (!empty($form_state['validation_complete'])) {
throw new \LogicException('Form errors cannot be set after form validation has finished.');
}
if (!isset($form_state['errors'][$name])) {
$record = TRUE;
if (isset($form_state['limit_validation_errors'])) {
// #limit_validation_errors is an array of "sections" within which user
// input must be valid. If the element is within one of these sections,
// the error must be recorded. Otherwise, it can be suppressed.
// #limit_validation_errors can be an empty array, in which case all
// errors are suppressed. For example, a "Previous" button might want
// its submit action to be triggered even if none of the submitted
// values are valid.
$record = FALSE;
foreach ($form_state['limit_validation_errors'] as $section) {
// Exploding by '][' reconstructs the element's #parents. If the
// reconstructed #parents begin with the same keys as the specified
// section, then the element's values are within the part of
// $form_state['values'] that the clicked button requires to be valid,
// so errors for this element must be recorded. As the exploded array
// will all be strings, we need to cast every value of the section
// array to string.
if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
$record = TRUE;
break;
}
}
}
if ($record) {
$form_state['errors'][$name] = $message;
$request = $this->requestStack->getCurrentRequest();
$request->attributes->set('_form_errors', TRUE);
if ($message) {
$this->drupalSetMessage($message, 'error');
}
}
}
return $form_state['errors'];
$this->formValidator->setErrorByName($name, $form_state, $message);
}
/**
* {@inheritdoc}
*/
public function clearErrors(array &$form_state) {
$form_state['errors'] = array();
$request = $this->requestStack->getCurrentRequest();
$request->attributes->set('_form_errors', FALSE);
$this->formValidator->clearErrors($form_state);
}
/**
* {@inheritdoc}
*/
public function getErrors(array $form_state) {
return $form_state['errors'];
return $this->formValidator->getErrors($form_state);
}
/**
* {@inheritdoc}
*/
public function getAnyErrors() {
$request = $this->requestStack->getCurrentRequest();
return (bool) $request->attributes->get('_form_errors');
return $this->formValidator->getAnyErrors();
}
/**
* {@inheritdoc}
*/
public function getError($element, array &$form_state) {
if ($errors = $this->getErrors($form_state)) {
$parents = array();
foreach ($element['#parents'] as $parent) {
$parents[] = $parent;
$key = implode('][', $parents);
if (isset($errors[$key])) {
return $errors[$key];
}
}
}
return $this->formValidator->getError($element, $form_state);
}
/**
* {@inheritdoc}
*/
public function setError(&$element, array &$form_state, $message = '') {
$this->setErrorByName(implode('][', $element['#parents']), $form_state, $message);
$this->formValidator->setError($element, $form_state, $message);
}
/**
@ -1693,37 +1351,6 @@ class FormBuilder implements FormBuilderInterface {
NestedArray::setValue($form_state['values'], $element['#parents'], $value, TRUE);
}
/**
* {@inheritdoc}
*/
public function flattenOptions(array $array) {
$this->flattenedOptions = array();
$this->doFlattenOptions($array);
return $this->flattenedOptions;
}
/**
* Iterates over an array building a flat array with duplicate keys removed.
*
* This function also handles cases where objects are passed as array values.
*
* @param array $array
* The form options array to process.
*/
protected function doFlattenOptions(array $array) {
foreach ($array as $key => $value) {
if (is_object($value)) {
$this->doFlattenOptions($value->option);
}
elseif (is_array($value)) {
$this->doFlattenOptions($value);
}
else {
$this->flattenedOptions[$key] = 1;
}
}
}
/**
* Triggers kernel.response and sends a form response.
*
@ -1760,22 +1387,6 @@ class FormBuilder implements FormBuilderInterface {
return drupal_installation_attempted();
}
/**
* Wraps watchdog().
*/
protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
watchdog($type, $message, $variables, $severity, $link);
}
/**
* Wraps drupal_set_message().
*
* @return array|null
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
return drupal_set_message($message, $type, $repeat);
}
/**
* Wraps drupal_html_class().
*

View File

@ -7,8 +7,6 @@
namespace Drupal\Core\Form;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an interface for form building and processing.
*/
@ -387,33 +385,6 @@ interface FormBuilderInterface extends FormErrorInterface {
*/
public function prepareForm($form_id, &$form, &$form_state);
/**
* Validates user-submitted form data in the $form_state array.
*
* @param $form_id
* A unique string identifying the form for validation, submission,
* theming, and hook_form_alter functions.
* @param $form
* An associative array containing the structure of the form, which is
* passed by reference. Form validation handlers are able to alter the form
* structure (like #process and #after_build callbacks during form building)
* in case of a validation error. If a validation handler alters the form
* structure, it is responsible for validating the values of changed form
* elements in $form_state['values'] to prevent form submit handlers from
* receiving unvalidated values.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also use
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* 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.
*/
public function validateForm($form_id, &$form, &$form_state);
/**
* Redirects the user to a URL after a form has been processed.
*
@ -477,14 +448,11 @@ interface FormBuilderInterface extends FormErrorInterface {
public function redirectForm($form_state);
/**
* Executes custom validation and submission handlers for a given form.
* Executes custom submission handlers for a given form.
*
* Button-specific handlers are checked first. If none exist, the function
* falls back to form-level handlers.
*
* @param $type
* The type of handler to execute. 'validate' or 'submit' are the
* defaults used by Form API.
* @param $form
* An associative array containing the structure of the form.
* @param $form_state
@ -492,7 +460,7 @@ interface FormBuilderInterface extends FormErrorInterface {
* submitted the form by clicking a button with custom handler functions
* defined, those handlers will be stored here.
*/
public function executeHandlers($type, &$form, &$form_state);
public function executeSubmitHandlers(&$form, &$form_state);
/**
* Builds and processes all elements in the structured form array.
@ -620,19 +588,4 @@ interface FormBuilderInterface extends FormErrorInterface {
*/
public function setValue($element, $value, &$form_state);
/**
* Allows PHP array processing of multiple select options with the same value.
*
* Used for form select elements which need to validate HTML option groups
* and multiple options which may return the same value. Associative PHP
* arrays cannot handle these structures, since they share a common key.
*
* @param array $array
* The form options array to process.
*
* @return array
* An array with all hierarchical elements flattened to a single array.
*/
public function flattenOptions(array $array);
}

View File

@ -0,0 +1,518 @@
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormValidator.
*/
namespace Drupal\Core\Form;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
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;
/**
* 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.
*/
public function __construct(RequestStack $request_stack, TranslationInterface $string_translation, CsrfTokenGenerator $csrf_token) {
$this->requestStack = $request_stack;
$this->stringTranslation = $string_translation;
$this->csrfToken = $csrf_token;
}
/**
* {@inheritdoc}
*/
public function executeValidateHandlers(&$form, &$form_state) {
// If there was a button pressed, use its handlers.
if (isset($form_state['validate_handlers'])) {
$handlers = $form_state['validate_handlers'];
}
// Otherwise, check for a form-level handler.
elseif (isset($form['#validate'])) {
$handlers = $form['#validate'];
}
else {
$handlers = array();
}
foreach ($handlers as $function) {
call_user_func_array($function, array(&$form, &$form_state));
}
}
/**
* {@inheritdoc}
*/
public function validateForm($form_id, &$form, &$form_state) {
// If this form is flagged to always validate, ensure that previous runs of
// validation are ignored.
if (!empty($form_state['must_validate'])) {
$form_state['validation_complete'] = FALSE;
}
// If this form has completed validation, do not validate again.
if (!empty($form_state['validation_complete'])) {
return;
}
// If the session token was set by self::prepareForm(), ensure that it
// matches the current user's session.
if (isset($form['#token'])) {
if (!$this->csrfToken->validate($form_state['values']['form_token'], $form['#token'])) {
$url = $this->requestStack->getCurrentRequest()->getRequestUri();
// Setting this error will cause the form to fail validation.
$this->setErrorByName('form_token', $form_state, $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
// 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);
}
/**
* 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.
* @param array $form_state
* An associative array containing the current state of the form.
* @param string $form_id
* The unique string identifying the form.
*/
protected function handleErrorsWithLimitedValidation(&$form, &$form_state, $form_id) {
// If validation errors are limited then remove any non validated form values,
// so that only values that passed validation are left for submit callbacks.
if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) {
$values = array();
foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) {
// If the section exists within $form_state['values'], even if the value
// is NULL, copy it to $values.
$section_exists = NULL;
$value = NestedArray::getValue($form_state['values'], $section, $section_exists);
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
// $form_state['values'] locations, even if these locations are not
// included in #limit_validation_errors.
if (!empty($form_state['triggering_element']['#is_button'])) {
$button_value = $form_state['triggering_element']['#value'];
// 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.
$parents = $form_state['triggering_element']['#parents'];
if (!NestedArray::keyExists($values, $parents) && NestedArray::getValue($form_state['values'], $parents) === $button_value) {
NestedArray::setValue($values, $parents, $button_value);
}
// Additionally, self::doBuildForm() places the button value in
// $form_state['values'][BUTTON_NAME]. If it's still there, after
// validation handlers have run, copy it to $values, but do not override
// what may already be in $values.
$name = $form_state['triggering_element']['#name'];
if (!isset($values[$name]) && isset($form_state['values'][$name]) && $form_state['values'][$name] === $button_value) {
$values[$name] = $button_value;
}
}
$form_state['values'] = $values;
}
}
/**
* Finalizes validation.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* An associative array containing the current state of the form.
* @param string $form_id
* The unique string identifying the form.
*/
protected function finalizeValidation(&$form, &$form_state, $form_id) {
// After validation, loop through and assign each element its errors.
$this->setElementErrorsFromFormState($form, $form_state);
// Mark this form as validated.
$form_state['validation_complete'] = TRUE;
}
/**
* 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.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* 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,
* theming, and hook_form_alter functions.
*/
protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
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.
$form_state['limit_validation_errors'] = $this->determineLimitValidationErrors($form_state);
// 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.
$is_empty_multiple = (!count($elements['#value']));
$is_empty_string = (is_string($elements['#value']) && Unicode::strlen(trim($elements['#value'])) == 0);
$is_empty_value = ($elements['#value'] === 0);
if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
// 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) {
call_user_func_array($callback, array(&$elements, &$form_state, &$form_state['complete_form']));
}
}
// 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.
if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value)) {
if (isset($elements['#required_error'])) {
$this->setError($elements, $form_state, $elements['#required_error']);
}
// 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'])) {
$this->setError($elements, $form_state, $this->t('!name field is required.', array('!name' => $elements['#title'])));
}
else {
$this->setError($elements, $form_state);
}
}
$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.
$form_state['limit_validation_errors'] = NULL;
}
/**
* Performs validation of elements that are not subject to limited validation.
*
* @param array $elements
* An associative array containing the structure of the form.
* @param array $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* 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.
*/
protected function performRequiredValidation(&$elements, &$form_state) {
// Verify that the value is not longer than #maxlength.
if (isset($elements['#maxlength']) && Unicode::strlen($elements['#value']) > $elements['#maxlength']) {
$this->setError($elements, $form_state, $this->t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => Unicode::strlen($elements['#value']))));
}
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'])) {
$value = in_array($elements['#type'], array('checkboxes', 'tableselect')) ? array_keys($elements['#value']) : $elements['#value'];
foreach ($value as $v) {
if (!isset($options[$v])) {
$this->setError($elements, $form_state, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$this->watchdog('form', 'Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
}
}
}
// 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.
// @see form_process_select()
elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
$elements['#value'] = NULL;
NestedArray::setValue($form_state['values'], $elements['#parents'], NULL, TRUE);
}
elseif (!isset($options[$elements['#value']])) {
$this->setError($elements, $form_state, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$this->watchdog('form', 'Illegal choice %choice in %name element.', array('%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']), WATCHDOG_ERROR);
}
}
}
/**
* Determines if validation errors should be limited.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return array|null
*/
protected function determineLimitValidationErrors(&$form_state) {
// While this element is being validated, it may be desired that some
// calls to self::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.
if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) {
return $form_state['triggering_element']['#limit_validation_errors'];
}
// 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
// types, #limit_validation_errors defaults to FALSE (via
// system_element_info()), so that full validation is their default
// behavior.
elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) {
return array();
}
// 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;
}
}
/**
* Stores the errors of each element directly on the element.
*
* Because self::getError() and self::getErrors() require the $form_state,
* we must provide a way for non-form functions to check the errors for a
* specific element. The most common usage of this is a #pre_render callback.
*
* @param array $elements
* An associative array containing the structure of a form element.
* @param array $form_state
* An associative array containing the current state of the form.
*/
protected function setElementErrorsFromFormState(array &$elements, array &$form_state) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
$this->setElementErrorsFromFormState($elements[$key], $form_state);
}
}
// Store the errors for this element on the element directly.
$elements['#errors'] = $this->getError($elements, $form_state);
}
/**
* {@inheritdoc}
*/
public function setErrorByName($name, array &$form_state, $message = '') {
if (!empty($form_state['validation_complete'])) {
throw new \LogicException('Form errors cannot be set after form validation has finished.');
}
if (!isset($form_state['errors'][$name])) {
$record = TRUE;
if (isset($form_state['limit_validation_errors'])) {
// #limit_validation_errors is an array of "sections" within which user
// input must be valid. If the element is within one of these sections,
// the error must be recorded. Otherwise, it can be suppressed.
// #limit_validation_errors can be an empty array, in which case all
// errors are suppressed. For example, a "Previous" button might want
// its submit action to be triggered even if none of the submitted
// values are valid.
$record = FALSE;
foreach ($form_state['limit_validation_errors'] as $section) {
// Exploding by '][' reconstructs the element's #parents. If the
// reconstructed #parents begin with the same keys as the specified
// section, then the element's values are within the part of
// $form_state['values'] that the clicked button requires to be valid,
// so errors for this element must be recorded. As the exploded array
// will all be strings, we need to cast every value of the section
// array to string.
if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) {
$record = TRUE;
break;
}
}
}
if ($record) {
$form_state['errors'][$name] = $message;
$this->requestStack->getCurrentRequest()->attributes->set('_form_errors', TRUE);
if ($message) {
$this->drupalSetMessage($message, 'error');
}
}
}
return $form_state['errors'];
}
/**
* {@inheritdoc}
*/
public function setError(&$element, array &$form_state, $message = '') {
$this->setErrorByName(implode('][', $element['#parents']), $form_state, $message);
}
/**
* {@inheritdoc}
*/
public function getError($element, array &$form_state) {
if ($errors = $this->getErrors($form_state)) {
$parents = array();
foreach ($element['#parents'] as $parent) {
$parents[] = $parent;
$key = implode('][', $parents);
if (isset($errors[$key])) {
return $errors[$key];
}
}
}
}
/**
* {@inheritdoc}
*/
public function clearErrors(array &$form_state) {
$form_state['errors'] = array();
$this->requestStack->getCurrentRequest()->attributes->set('_form_errors', FALSE);
}
/**
* {@inheritdoc}
*/
public function getErrors(array $form_state) {
return $form_state['errors'];
}
/**
* {@inheritdoc}
*/
public function getAnyErrors() {
return (bool) $this->requestStack->getCurrentRequest()->attributes->get('_form_errors');
}
/**
* Wraps watchdog().
*/
protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
watchdog($type, $message, $variables, $severity, $link);
}
/**
* Wraps drupal_set_message().
*
* @return array|null
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
return drupal_set_message($message, $type, $repeat);
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormValidatorInterface.
*/
namespace Drupal\Core\Form;
/**
* Provides an interface for validating form submissions.
*/
interface FormValidatorInterface extends FormErrorInterface {
/**
* Executes custom validation handlers for a given form.
*
* Button-specific handlers are checked first. If none exist, the function
* falls back to form-level handlers.
*
* @param $form
* An associative array containing the structure of the form.
* @param $form_state
* A keyed array containing the current state of the form. If the user
* submitted the form by clicking a button with custom handler functions
* defined, those handlers will be stored here.
*/
public function executeValidateHandlers(&$form, &$form_state);
/**
* Validates user-submitted form data in the $form_state array.
*
* @param $form_id
* A unique string identifying the form for validation, submission,
* theming, and hook_form_alter functions.
* @param $form
* An associative array containing the structure of the form, which is
* passed by reference. Form validation handlers are able to alter the form
* structure (like #process and #after_build callbacks during form building)
* in case of a validation error. If a validation handler alters the form
* structure, it is responsible for validating the values of changed form
* elements in $form_state['values'] to prevent form submit handlers from
* receiving unvalidated values.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also use
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* 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.
*/
public function validateForm($form_id, &$form, &$form_state);
}

View File

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\Core\Form\OptGroup.
*/
namespace Drupal\Core\Form;
/**
* Provides helpers for HTML option groups.
*/
class OptGroup {
/**
* Allows PHP array processing of multiple select options with the same value.
*
* Used for form select elements which need to validate HTML option groups
* and multiple options which may return the same value. Associative PHP
* arrays cannot handle these structures, since they share a common key.
*
* @param array $array
* The form options array to process.
*
* @return array
* An array with all hierarchical elements flattened to a single array.
*/
public static function flattenOptions(array $array) {
$options = array();
static::doFlattenOptions($array, $options);
return $options;
}
/**
* Iterates over an array building a flat array with duplicate keys removed.
*
* This function also handles cases where objects are passed as array values.
*
* @param array $array
* The form options array to process.
* @param array $options
* The array of flattened options.
*/
protected static function doFlattenOptions(array $array, array &$options) {
foreach ($array as $key => $value) {
if (is_object($value)) {
static::doFlattenOptions($value->option, $options);
}
elseif (is_array($value)) {
static::doFlattenOptions($value, $options);
}
else {
$options[$key] = 1;
}
}
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\entity_reference;
use Drupal\Component\Utility\String;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
use Drupal\Core\TypedData\DataDefinition;
@ -69,7 +70,7 @@ class ConfigurableEntityReferenceItem extends EntityReferenceItem implements All
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options first, because "settable options" may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}

View File

@ -8,6 +8,7 @@
namespace Drupal\options\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
@ -32,7 +33,7 @@ abstract class ListItemBase extends FieldItemBase implements AllowedValuesInterf
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Possible Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getPossibleOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
@ -49,7 +50,7 @@ abstract class ListItemBase extends FieldItemBase implements AllowedValuesInterf
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}

View File

@ -9,6 +9,7 @@ namespace Drupal\taxonomy\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
@ -48,7 +49,7 @@ class TaxonomyTermReferenceItem extends EntityReferenceItem implements AllowedVa
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Possible Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getPossibleOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
@ -65,7 +66,7 @@ class TaxonomyTermReferenceItem extends EntityReferenceItem implements AllowedVa
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}

View File

@ -9,6 +9,7 @@ namespace Drupal\Tests\Core\Form {
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -220,34 +221,6 @@ class FormBuilderTest extends FormTestBase {
$this->assertSame($response, $form_state['response']);
}
/**
* Tests that form errors during submission throw an exception.
*
* @covers ::setErrorByName
*
* @expectedException \LogicException
* @expectedExceptionMessage Form errors cannot be set after form validation has finished.
*/
public function testFormErrorsDuringSubmission() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_builder = $this->formBuilder;
$form_arg->expects($this->any())
->method('submitForm')
->will($this->returnCallback(function ($form, &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'Hello');
}));
$form_state = array();
$this->formBuilder->getFormId($form_arg, $form_state);
$form_state['values'] = array();
$form_state['input']['form_id'] = $form_id;
$this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);
}
/**
* Tests the redirectForm() method when a redirect is expected.
*
@ -499,124 +472,13 @@ class FormBuilderTest extends FormTestBase {
$this->assertNotSame($original_build_id, $form['#build_id']);
}
/**
* Tests the submitForm() method.
*/
public function testSubmitForm() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['test']['#required'] = TRUE;
$expected_form['options']['#required'] = TRUE;
$expected_form['value']['#required'] = TRUE;
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->exactly(5))
->method('getFormId')
->will($this->returnValue($form_id));
$form_arg->expects($this->exactly(5))
->method('buildForm')
->will($this->returnValue($expected_form));
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = array('foo');
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = array('foo', 'baz');
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = $this->randomName();
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
}
/**
* Tests the 'must_validate' $form_state flag.
*
* @covers ::validateForm
*/
public function testMustValidate() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->any())
->method('getFormId')
->will($this->returnValue($form_id));
$form_arg->expects($this->any())
->method('buildForm')
->will($this->returnValue($expected_form));
$form_builder = $this->formBuilder;
$form_arg->expects($this->exactly(2))
->method('validateForm')
->will($this->returnCallback(function (&$form, &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'foo');
}));
$form_state = array();
// This submission will trigger validation.
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
// This submission will not re-trigger validation.
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
// The must_validate flag will re-trigger validation.
$form_state['must_validate'] = TRUE;
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
}
/**
* Tests the flattenOptions() method.
*
* @dataProvider providerTestFlattenOptions
*/
public function testFlattenOptions($options) {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['select']['#required'] = TRUE;
$expected_form['select']['#options'] = $options;
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_state = array();
$form_state['values']['select'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$this->assertSame(array('foo' => 1), OptGroup::flattenOptions($options));
}
/**
@ -625,100 +487,15 @@ class FormBuilderTest extends FormTestBase {
* @return array
*/
public function providerTestFlattenOptions() {
$object = new \stdClass();
$object->option = array('foo' => 'foo');
$object1 = new \stdClass();
$object1->option = array('foo' => 'foo');
$object2 = new \stdClass();
$object2->option = array(array('foo' => 'foo'), array('foo' => 'foo'));
return array(
array(array('foo' => 'foo')),
array(array(array('foo' => 'foo'))),
array(array($object)),
);
}
/**
* Tests the setErrorByName() method.
*
* @param array|null $limit_validation_errors
* The errors to limit validation for, NULL will run all validation.
* @param array $expected_errors
* The errors expected to be set.
*
* @dataProvider providerTestSetErrorByName
*/
public function testSetErrorByName($limit_validation_errors, $expected_errors) {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['actions']['submit']['#submit'][] = 'test_form_id_custom_submit';
$expected_form['actions']['submit']['#limit_validation_errors'] = $limit_validation_errors;
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_builder = $this->formBuilder;
$form_arg->expects($this->once())
->method('validateForm')
->will($this->returnCallback(function (array &$form, array &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'Fail 1');
$form_builder->setErrorByName('test', $form_state, 'Fail 2');
$form_builder->setErrorByName('options', $form_state);
}));
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertSame($expected_errors, $errors);
}
/**
* Provides test data for testing the setErrorByName() method.
*
* @return array
* Returns some test data.
*/
public function providerTestSetErrorByName() {
return array(
// Only validate the 'options' element.
array(array(array('options')), array('options' => '')),
// Do not limit an validation, and, ensuring the first error is returned
// for the 'test' element.
array(NULL, array('test' => 'Fail 1', 'options' => '')),
// Limit all validation.
array(array(), array()),
);
}
/**
* Tests the getError() method.
*
* @dataProvider providerTestGetError
*/
public function testGetError($parents, $expected = NULL) {
$form_state = array();
// Set errors on a top level and a child element, and a nested element.
$this->formBuilder->setErrorByName('foo', $form_state, 'Fail 1');
$this->formBuilder->setErrorByName('foo][bar', $form_state, 'Fail 2');
$this->formBuilder->setErrorByName('baz][bim', $form_state, 'Fail 3');
$element['#parents'] = $parents;
$error = $this->formBuilder->getError($element, $form_state);
$this->assertSame($expected, $error);
}
/**
* Provides test data for testing the getError() method.
*
* @return array
* Returns some test data.
*/
public function providerTestGetError() {
return array(
array(array('foo'), 'Fail 1'),
array(array('foo', 'bar'), 'Fail 1'),
array(array('baz')),
array(array('baz', 'bim'), 'Fail 3'),
array(array($this->randomName())),
array(array()),
array(array($object1)),
array(array($object2)),
);
}
@ -774,8 +551,7 @@ class FormBuilderTest extends FormTestBase {
$form_state['input']['form_id'] = $form_id;
$form_state['input']['form_build_id'] = $form['#build_id'];
$this->formBuilder->buildForm($form_id, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$this->assertEmpty($form_state['errors']);
}
/**
@ -799,6 +575,32 @@ class FormBuilderTest extends FormTestBase {
$this->formBuilder->buildForm($form_arg, $form_state);
}
/**
* Tests that HTML IDs are unique when rebuilding a form with errors.
*/
public function testUniqueHtmlId() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['test']['#required'] = TRUE;
$this->formValidator->expects($this->exactly(4))
->method('getAnyErrors')
->will($this->returnValue(TRUE));
// Mock a form object that will be built two times.
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->exactly(2))
->method('buildForm')
->will($this->returnValue($expected_form));
$form_state = array();
$form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
$this->assertSame($form_id, $form['#id']);
$form_state = array();
$form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
$this->assertSame("$form_id--2", $form['#id']);
}
}
class TestForm implements FormInterface {

View File

@ -29,6 +29,11 @@ abstract class FormTestBase extends UnitTestCase {
*/
protected $formBuilder;
/**
* @var \Drupal\Core\Form\FormValidatorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $formValidator;
/**
* The mocked URL generator.
*
@ -92,11 +97,6 @@ abstract class FormTestBase extends UnitTestCase {
*/
protected $keyValueExpirableFactory;
/**
* @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translationManager;
/**
* @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\HttpKernel
*/
@ -118,8 +118,8 @@ abstract class FormTestBase extends UnitTestCase {
)));
$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->formValidator = $this->getMock('Drupal\Core\Form\FormValidatorInterface');
$this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
$this->translationManager = $this->getStringTranslationStub();
$this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
@ -145,7 +145,7 @@ abstract class FormTestBase extends UnitTestCase {
protected function setupFormBuilder() {
$request_stack = new RequestStack();
$request_stack->push($this->request);
$this->formBuilder = new TestFormBuilder($this->moduleHandler, $this->keyValueExpirableFactory, $this->eventDispatcher, $this->urlGenerator, $this->translationManager, $request_stack, $this->csrfToken, $this->httpKernel);
$this->formBuilder = new TestFormBuilder($this->formValidator, $this->moduleHandler, $this->keyValueExpirableFactory, $this->eventDispatcher, $this->urlGenerator, $request_stack, $this->csrfToken, $this->httpKernel);
$this->formBuilder->setCurrentUser($this->account);
}
@ -283,18 +283,6 @@ class TestFormBuilder extends FormBuilder {
return FALSE;
}
/**
* {@inheritdoc}
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
}
/**
* {@inheritdoc}
*/
protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
}
/**
* {@inheritdoc}
*/

View File

@ -1,47 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Form\FormValidationTest.
*/
namespace Drupal\Tests\Core\Form;
/**
* Tests various form element validation mechanisms.
*
* @group Drupal
* @group Form
*/
class FormValidationTest extends FormTestBase {
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Form element validation',
'description' => 'Tests various form element validation mechanisms.',
'group' => 'Form API',
);
}
public function testUniqueHtmlId() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['test']['#required'] = TRUE;
// Mock a form object that will be built three times.
$form_arg = $this->getMockForm($form_id, $expected_form, 2);
$form_state = array();
$this->formBuilder->getFormId($form_arg, $form_state);
$form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
$this->assertSame($form_id, $form['#id']);
$form_state = array();
$form = $this->simulateFormSubmission($form_id, $form_arg, $form_state);
$this->assertSame("$form_id--2", $form['#id']);
}
}

View File

@ -0,0 +1,624 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Form\FormValidatorTest.
*/
namespace Drupal\Tests\Core\Form {
use Drupal\Component\Utility\String;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Tests the form validator.
*
* @coversDefaultClass \Drupal\Core\Form\FormValidator
*
* @group Drupal
* @group Form
*/
class FormValidatorTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Form validator test',
'description' => 'Tests the form validator.',
'group' => 'Form API',
);
}
/**
* Tests that form errors during submission throw an exception.
*
* @covers ::setErrorByName
*
* @expectedException \LogicException
* @expectedExceptionMessage Form errors cannot be set after form validation has finished.
*/
public function testFormErrorsDuringSubmission() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
$form_state['validation_complete'] = TRUE;
$form_validator->setErrorByName('test', $form_state, 'message');
}
/**
* Tests the 'validation_complete' $form_state flag.
*
* @covers ::validateForm
* @covers ::finalizeValidation
*/
public function testValidationComplete() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
$form = array();
$form_state = $this->getFormStateDefaults();
$this->assertFalse($form_state['validation_complete']);
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertTrue($form_state['validation_complete']);
}
/**
* Tests the 'must_validate' $form_state flag.
*
* @covers ::validateForm
*/
public function testPreventDuplicateValidation() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(array('doValidateForm'))
->getMock();
$form_validator->expects($this->never())
->method('doValidateForm');
$form = array();
$form_state = $this->getFormStateDefaults();
$form_state['validation_complete'] = TRUE;
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertArrayNotHasKey('#errors', $form);
}
/**
* Tests the 'must_validate' $form_state flag.
*
* @covers ::validateForm
*/
public function testMustValidate() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(array('doValidateForm'))
->getMock();
$form_validator->expects($this->once())
->method('doValidateForm');
$form = array();
$form_state = $this->getFormStateDefaults();
$form_state['validation_complete'] = TRUE;
$form_state['must_validate'] = TRUE;
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertArrayHasKey('#errors', $form);
}
/**
* @covers ::validateForm
*/
public function testValidateInvalidFormToken() {
$request_stack = new RequestStack();
$request = new Request(array(), array(), array(), array(), array(), array('REQUEST_URI' => '/test/example?foo=bar'));
$request_stack->push($request);
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$csrf_token->expects($this->once())
->method('validate')
->will($this->returnValue(FALSE));
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('setErrorByName', 'doValidateForm'))
->getMock();
$form_validator->expects($this->once())
->method('setErrorByName')
->with('form_token', $this->isType('array'), 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');
$form_validator->expects($this->never())
->method('doValidateForm');
$form['#token'] = 'test_form_id';
$form_state = $this->getFormStateDefaults();
$form_state['values']['form_token'] = 'some_random_token';
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertTrue($form_state['validation_complete']);
}
/**
* @covers ::validateForm
*/
public function testValidateValidFormToken() {
$request_stack = new RequestStack();
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$csrf_token->expects($this->once())
->method('validate')
->will($this->returnValue(TRUE));
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('setErrorByName', 'doValidateForm'))
->getMock();
$form_validator->expects($this->never())
->method('setErrorByName');
$form_validator->expects($this->once())
->method('doValidateForm');
$form['#token'] = 'test_form_id';
$form_state = $this->getFormStateDefaults();
$form_state['values']['form_token'] = 'some_random_token';
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertTrue($form_state['validation_complete']);
}
/**
* Tests the setError() method.
*
* @covers ::setError
*/
public function testSetError() {
$form_state = $this->getFormStateDefaults();
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(array('setErrorByName'))
->getMock();
$form_validator->expects($this->once())
->method('setErrorByName')
->with('foo][bar', $form_state, 'Fail');
$element['#parents'] = array('foo', 'bar');
$form_validator->setError($element, $form_state, 'Fail');
}
/**
* Tests the getError() method.
*
* @covers ::getError
*
* @dataProvider providerTestGetError
*/
public function testGetError($errors, $parents, $error = NULL) {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
$element['#parents'] = $parents;
$form_state = $this->getFormStateDefaults();
$form_state['errors'] = $errors;
$this->assertSame($error, $form_validator->getError($element, $form_state));
}
public function providerTestGetError() {
return array(
array(array(), array('foo')),
array(array('foo][bar' => 'Fail'), array()),
array(array('foo][bar' => 'Fail'), array('foo')),
array(array('foo][bar' => 'Fail'), array('bar')),
array(array('foo][bar' => 'Fail'), array('baz')),
array(array('foo][bar' => 'Fail'), array('foo', 'bar'), 'Fail'),
array(array('foo][bar' => 'Fail'), array('foo', 'bar', 'baz'), 'Fail'),
array(array('foo][bar' => 'Fail 2'), array('foo')),
array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo'), 'Fail 1'),
array(array('foo' => 'Fail 1', 'foo][bar' => 'Fail 2'), array('foo', 'bar'), 'Fail 1'),
);
}
/**
* @covers ::setErrorByName
*
* @dataProvider providerTestSetErrorByName
*/
public function testSetErrorByName($limit_validation_errors, $expected_errors, $set_message = FALSE) {
$request_stack = new RequestStack();
$request = new Request();
$request_stack->push($request);
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('drupalSetMessage'))
->getMock();
$form_validator->expects($set_message ? $this->once() : $this->never())
->method('drupalSetMessage');
$form_state = $this->getFormStateDefaults();
$form_state['limit_validation_errors'] = $limit_validation_errors;
$form_validator->setErrorByName('test', $form_state, 'Fail 1');
$form_validator->setErrorByName('test', $form_state, 'Fail 2');
$form_validator->setErrorByName('options', $form_state);
$this->assertSame(!empty($expected_errors), $request->attributes->get('_form_errors', FALSE));
$this->assertSame($expected_errors, $form_state['errors']);
}
public function providerTestSetErrorByName() {
return array(
// Only validate the 'options' element.
array(array(array('options')), array('options' => '')),
// Do not limit an validation, and, ensuring the first error is returned
// for the 'test' element.
array(NULL, array('test' => 'Fail 1', 'options' => ''), TRUE),
// Limit all validation.
array(array(), array()),
);
}
/**
* @covers ::setElementErrorsFromFormState
*/
public function testSetElementErrorsFromFormState() {
$request_stack = new RequestStack();
$request = new Request();
$request_stack->push($request);
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array($request_stack, $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('drupalSetMessage'))
->getMock();
$form = array(
'#parents' => array(),
);
$form['test'] = array(
'#type' => 'textfield',
'#title' => 'Test',
'#parents' => array('test'),
);
$form_state = $this->getFormStateDefaults();
$form_validator->setErrorByName('test', $form_state, 'invalid');
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertSame('invalid', $form['test']['#errors']);
}
/**
* @covers ::handleErrorsWithLimitedValidation
*
* @dataProvider providerTestHandleErrorsWithLimitedValidation
*/
public function testHandleErrorsWithLimitedValidation($sections, $triggering_element, $values, $expected) {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
$form = array();
$form_state = $this->getFormStateDefaults();
$form_state['triggering_element'] = $triggering_element;
$form_state['triggering_element']['#limit_validation_errors'] = $sections;
$form_state['values'] = $values;
$form_validator->validateForm('test_form_id', $form, $form_state);
$this->assertSame($expected, $form_state['values']);
}
public function providerTestHandleErrorsWithLimitedValidation() {
return array(
// Test with a non-existent section.
array(
array(array('test1'), array('test3')),
array(),
array(
'test1' => 'foo',
'test2' => 'bar',
),
array(
'test1' => 'foo',
),
),
// Test with buttons in a non-validated section.
array(
array(array('test1')),
array(
'#is_button' => true,
'#value' => 'baz',
'#name' => 'op',
'#parents' => array('submit'),
),
array(
'test1' => 'foo',
'test2' => 'bar',
'op' => 'baz',
'submit' => 'baz',
),
array(
'test1' => 'foo',
'submit' => 'baz',
'op' => 'baz',
),
),
// Test with a matching button #value and $form_state value.
array(
array(array('submit')),
array(
'#is_button' => TRUE,
'#value' => 'baz',
'#name' => 'op',
'#parents' => array('submit'),
),
array(
'test1' => 'foo',
'test2' => 'bar',
'op' => 'baz',
'submit' => 'baz',
),
array(
'submit' => 'baz',
'op' => 'baz',
),
),
// Test with a mismatched button #value and $form_state value.
array(
array(array('submit')),
array(
'#is_button' => TRUE,
'#value' => 'bar',
'#name' => 'op',
'#parents' => array('submit'),
),
array(
'test1' => 'foo',
'test2' => 'bar',
'op' => 'baz',
'submit' => 'baz',
),
array(
'submit' => 'baz',
),
),
);
}
/**
* @covers ::executeValidateHandlers
*/
public function testExecuteValidateHandlers() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
$mock = $this->getMock('stdClass', array('validate_handler', 'hash_validate'));
$mock->expects($this->once())
->method('validate_handler')
->with($this->isType('array'), $this->isType('array'));
$mock->expects($this->once())
->method('hash_validate')
->with($this->isType('array'), $this->isType('array'));
$form = array();
$form_state = $this->getFormStateDefaults();
$form_validator->executeValidateHandlers($form, $form_state);
$form['#validate'][] = array($mock, 'hash_validate');
$form_validator->executeValidateHandlers($form, $form_state);
// $form_state validate handlers will supersede $form handlers.
$form_state['validate_handlers'][] = array($mock, 'validate_handler');
$form_validator->executeValidateHandlers($form, $form_state);
}
/**
* @covers ::doValidateForm
*
* @dataProvider providerTestRequiredErrorMessage
*/
public function testRequiredErrorMessage($element, $expected_message) {
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array(new RequestStack(), $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('executeValidateHandlers', 'setErrorByName'))
->getMock();
$form_validator->expects($this->once())
->method('executeValidateHandlers');
$form_validator->expects($this->once())
->method('setErrorByName')
->with('test', $this->isType('array'), $expected_message);
$form = array();
$form['test'] = $element + array(
'#type' => 'textfield',
'#value' => '',
'#needs_validation' => TRUE,
'#required' => TRUE,
'#parents' => array('test'),
);
$form_state = $this->getFormStateDefaults();
$form_validator->validateForm('test_form_id', $form, $form_state);
}
public function providerTestRequiredErrorMessage() {
return array(
array(
// Use the default message with a title.
array('#title' => 'Test'),
'Test field is required.',
),
// Use a custom message.
array(
array('#required_error' => 'FAIL'),
'FAIL',
),
// No title or custom message.
array(
array(),
'',
),
);
}
/**
* @covers ::doValidateForm
*/
public function testElementValidate() {
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->disableOriginalConstructor()
->setMethods(array('executeValidateHandlers', 'setErrorByName'))
->getMock();
$form_validator->expects($this->once())
->method('executeValidateHandlers');
$mock = $this->getMock('stdClass', array('element_validate'));
$mock->expects($this->once())
->method('element_validate')
->with($this->isType('array'), $this->isType('array'), NULL);
$form = array();
$form['test'] = array(
'#type' => 'textfield',
'#title' => 'Test',
'#parents' => array('test'),
'#element_validate' => array(array($mock, 'element_validate')),
);
$form_state = $this->getFormStateDefaults();
$form_validator->validateForm('test_form_id', $form, $form_state);
}
/**
* @covers ::performRequiredValidation
*
* @dataProvider providerTestPerformRequiredValidation
*/
public function testPerformRequiredValidation($element, $expected_message, $call_watchdog) {
$csrf_token = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
->disableOriginalConstructor()
->getMock();
$form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
->setConstructorArgs(array(new RequestStack(), $this->getStringTranslationStub(), $csrf_token))
->setMethods(array('setErrorByName', 'watchdog'))
->getMock();
$form_validator->expects($this->once())
->method('setErrorByName')
->with('test', $this->isType('array'), $expected_message);
if ($call_watchdog) {
$form_validator->expects($this->once())
->method('watchdog')
->with('form');
}
$form = array();
$form['test'] = $element + array(
'#title' => 'Test',
'#needs_validation' => TRUE,
'#required' => FALSE,
'#parents' => array('test'),
);
$form_state = $this->getFormStateDefaults();
$form_state['values'] = array();
$form_validator->validateForm('test_form_id', $form, $form_state);
}
public function providerTestPerformRequiredValidation() {
return array(
array(
array(
'#type' => 'select',
'#options' => array(
'foo' => 'Foo',
'bar' => 'Bar',
),
'#required' => TRUE,
'#value' => 'baz',
'#empty_value' => 'baz',
'#multiple' => FALSE,
),
'Test field is required.',
FALSE,
),
array(
array(
'#type' => 'select',
'#options' => array(
'foo' => 'Foo',
'bar' => 'Bar',
),
'#value' => 'baz',
'#multiple' => FALSE,
),
'An illegal choice has been detected. Please contact the site administrator.',
TRUE,
),
array(
array(
'#type' => 'checkboxes',
'#options' => array(
'foo' => 'Foo',
'bar' => 'Bar',
),
'#value' => array('baz'),
'#multiple' => TRUE,
),
'An illegal choice has been detected. Please contact the site administrator.',
TRUE,
),
array(
array(
'#type' => 'select',
'#options' => array(
'foo' => 'Foo',
'bar' => 'Bar',
),
'#value' => array('baz'),
'#multiple' => TRUE,
),
'An illegal choice has been detected. Please contact the site administrator.',
TRUE,
),
array(
array(
'#type' => 'textfield',
'#maxlength' => 7,
'#value' => $this->randomName(8),
),
String::format('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => 'Test', '%max' => '7', '%length' => 8)),
FALSE,
),
);
}
/**
* @return array()
*/
protected function getFormStateDefaults() {
$form_builder = $this->getMockBuilder('Drupal\Core\Form\FormBuilder')
->disableOriginalConstructor()
->setMethods(NULL)
->getMock();
return $form_builder->getFormStateDefaults();
}
}
}
namespace {
if (!defined('WATCHDOG_ERROR')) {
define('WATCHDOG_ERROR', 3);
}
if (!defined('WATCHDOG_NOTICE')) {
define('WATCHDOG_NOTICE', 5);
}
}