Issue #2209977 by tim.plunkett: Move form validation logic out of FormBuilder into a new class.
parent
02eb3d3fc9
commit
f710a6c92e
|
@ -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']
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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().
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue