Commit df1a669b authored by webchick's avatar webchick

Issue #2131851 by tim.plunkett: Form errors must be specific to a form and not a global.

parent 53804ea1
......@@ -210,7 +210,7 @@ function authorize_filetransfer_form_validate($form, &$form_state) {
catch (Exception $e) {
// The format of this error message is similar to that used on the
// database connection form in the installer.
form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array(
form_set_error('connection_settings', $form_state, t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array(
'!message' => '<p class="error">' . $e->getMessage() . '</p>',
'@handbook_url' => 'http://drupal.org/documentation/install/modules-themes',
)));
......
......@@ -274,8 +274,8 @@ function form_execute_handlers($type, &$form, &$form_state) {
*
* @deprecated as of Drupal 8.0. Use \Drupal::formBuilder()->setErrorByName()
*/
function form_set_error($name = NULL, $message = '', $limit_validation_errors = NULL) {
return \Drupal::formBuilder()->setErrorByName($name, $message, $limit_validation_errors);
function form_set_error($name, array &$form_state, $message = '') {
\Drupal::formBuilder()->setErrorByName($name, $form_state, $message);
}
/**
......@@ -283,8 +283,8 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors =
*
* @deprecated as of Drupal 8.0. Use \Drupal::formBuilder()->clearErrors()
*/
function form_clear_error() {
\Drupal::formBuilder()->clearErrors();
function form_clear_error(array &$form_state) {
\Drupal::formBuilder()->clearErrors($form_state);
}
/**
......@@ -292,8 +292,8 @@ function form_clear_error() {
*
* @deprecated as of Drupal 8.0. Use \Drupal::formBuilder()->getErrors()
*/
function form_get_errors() {
return \Drupal::formBuilder()->getErrors();
function form_get_errors(array &$form_state) {
return \Drupal::formBuilder()->getErrors($form_state);
}
/**
......@@ -301,8 +301,8 @@ function form_get_errors() {
*
* @deprecated as of Drupal 8.0. Use \Drupal::formBuilder()->getError()
*/
function form_get_error($element) {
return \Drupal::formBuilder()->getError($element);
function form_get_error($element, array &$form_state) {
return \Drupal::formBuilder()->getError($element, $form_state);
}
/**
......@@ -310,8 +310,8 @@ function form_get_error($element) {
*
* @deprecated as of Drupal 8.0. Use \Drupal::formBuilder()->setError()
*/
function form_error(&$element, $message = '') {
\Drupal::formBuilder()->setError($element, $message);
function form_error(&$element, array &$form_state, $message = '') {
\Drupal::formBuilder()->setError($element, $form_state, $message);
}
/**
......@@ -1108,11 +1108,11 @@ function password_confirm_validate($element, &$element_state) {
$pass2 = trim($element['pass2']['#value']);
if (!empty($pass1) || !empty($pass2)) {
if (strcmp($pass1, $pass2)) {
form_error($element, t('The specified passwords do not match.'));
form_error($element, $element_state, t('The specified passwords do not match.'));
}
}
elseif ($element['#required'] && !empty($element_state['input'])) {
form_error($element, t('Password field is required.'));
form_error($element, $element_state, t('Password field is required.'));
}
// Password field must be converted from a two-element array into a single
......@@ -1457,7 +1457,7 @@ function form_validate_pattern($element, &$form_state) {
$pattern = '{^(?:' . $element['#pattern'] . ')$}';
if (!preg_match($pattern, $element['#value'])) {
form_error($element, t('%name field is not in the right format.', array('%name' => $element['#title'])));
form_error($element, $form_state, t('%name field is not in the right format.', array('%name' => $element['#title'])));
}
}
}
......@@ -1767,11 +1767,11 @@ function form_validate_table($element, &$form_state) {
}
if ($element['#multiple']) {
if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
form_error($element, t('No items selected.'));
form_error($element, $form_state, t('No items selected.'));
}
}
elseif (!isset($element['#value']) || $element['#value'] === '') {
form_error($element, t('No item selected.'));
form_error($element, $form_state, t('No item selected.'));
}
}
......@@ -1894,7 +1894,7 @@ function form_process_machine_name($element, &$form_state) {
function form_validate_machine_name(&$element, &$form_state) {
// Verify that the machine name not only consists of replacement tokens.
if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
form_error($element, t('The machine-readable name must contain unique characters.'));
form_error($element, $form_state, t('The machine-readable name must contain unique characters.'));
}
// Verify that the machine name contains no disallowed characters.
......@@ -1903,15 +1903,15 @@ function form_validate_machine_name(&$element, &$form_state) {
// Since a hyphen is the most common alternative replacement character,
// a corresponding validation error message is supported here.
if ($element['#machine_name']['replace'] == '-') {
form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
}
// Otherwise, we assume the default (underscore).
else {
form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
}
else {
form_error($element, $element['#machine_name']['error']);
form_error($element, $form_state, $element['#machine_name']['error']);
}
}
......@@ -1919,7 +1919,7 @@ function form_validate_machine_name(&$element, &$form_state) {
if ($element['#default_value'] !== $element['#value']) {
$function = $element['#machine_name']['exists'];
if (call_user_func($function, $element['#value'], $element, $form_state)) {
form_error($element, t('The machine-readable name is already in use. It must be unique.'));
form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.'));
}
}
}
......@@ -2339,7 +2339,7 @@ function form_validate_email(&$element, &$form_state) {
form_set_value($element, $value, $form_state);
if ($value !== '' && !valid_email_address($value)) {
form_error($element, t('The e-mail address %mail is not valid.', array('%mail' => $value)));
form_error($element, $form_state, t('The e-mail address %mail is not valid.', array('%mail' => $value)));
}
}
......@@ -2415,18 +2415,18 @@ function form_validate_number(&$element, &$form_state) {
// Ensure the input is numeric.
if (!is_numeric($value)) {
form_error($element, t('%name must be a number.', array('%name' => $name)));
form_error($element, $form_state, t('%name must be a number.', array('%name' => $name)));
return;
}
// Ensure that the input is greater than the #min property, if set.
if (isset($element['#min']) && $value < $element['#min']) {
form_error($element, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min'])));
form_error($element, $form_state, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min'])));
}
// Ensure that the input is less than the #max property, if set.
if (isset($element['#max']) && $value > $element['#max']) {
form_error($element, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max'])));
form_error($element, $form_state, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max'])));
}
if (isset($element['#step']) && strtolower($element['#step']) != 'any') {
......@@ -2435,7 +2435,7 @@ function form_validate_number(&$element, &$form_state) {
$offset = isset($element['#min']) ? $element['#min'] : 0.0;
if (!Number::validStep($value, $element['#step'], $offset)) {
form_error($element, t('%name is not a valid number.', array('%name' => $name)));
form_error($element, $form_state, t('%name is not a valid number.', array('%name' => $name)));
}
}
}
......@@ -2518,7 +2518,7 @@ function form_validate_url(&$element, &$form_state) {
form_set_value($element, $value, $form_state);
if ($value !== '' && !valid_url($value, TRUE)) {
form_error($element, t('The URL %url is not valid.', array('%url' => $value)));
form_error($element, $form_state, t('The URL %url is not valid.', array('%url' => $value)));
}
}
......@@ -2539,7 +2539,7 @@ function form_validate_color(&$element, &$form_state) {
form_set_value($element, Color::rgbToHex(Color::hexToRgb($value)), $form_state);
}
catch (InvalidArgumentException $e) {
form_error($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'])));
form_error($element, $form_state, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'])));
}
}
}
......@@ -2908,7 +2908,7 @@ function _form_set_attributes(&$element, $class = array()) {
$element['#attributes']['required'] = 'required';
$element['#attributes']['aria-required'] = 'true';
}
if (isset($element['#parents']) && form_get_error($element) !== NULL && !empty($element['#validated'])) {
if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) {
$element['#attributes']['class'][] = 'error';
$element['#attributes']['aria-invalid'] = 'true';
}
......
......@@ -653,7 +653,7 @@ function install_run_task($task, &$install_state) {
'build_info' => array('args' => array(&$install_state)),
);
drupal_form_submit($function, $form_state);
$errors = form_get_errors();
$errors = form_get_errors($form_state);
if (!empty($errors)) {
throw new Exception(implode("\n", $errors));
}
......@@ -1192,7 +1192,7 @@ function install_settings_form_validate($form, &$form_state) {
$form_state['storage']['database'] = $database;
$errors = install_database_errors($database, $form_state['values']['settings_file']);
foreach ($errors as $name => $message) {
form_set_error($name, $message);
form_set_error($name, $form_state, $message);
}
}
......@@ -2699,7 +2699,7 @@ function _install_configure_form($form, &$form_state, &$install_state) {
*/
function install_configure_form_validate($form, &$form_state) {
if ($error = user_validate_name($form_state['values']['account']['name'])) {
form_error($form['admin_account']['account']['name'], $error);
form_error($form['admin_account']['account']['name'], $form_state, $error);
}
}
......
......@@ -359,7 +359,7 @@ public function flagErrors(FieldItemListInterface $items, array $form, array &$f
// @todo: Pass $violation->arrayPropertyPath as property path.
$error_element = $this->errorElement($delta_element, $violation, $form, $form_state);
if ($error_element !== FALSE) {
form_error($error_element, $violation->getMessage());
form_error($error_element, $form_state, $violation->getMessage());
}
}
}
......
......@@ -103,20 +103,6 @@ class FormBuilder implements FormBuilderInterface {
*/
protected $forms;
/**
* An array of form errors.
*
* @var array
*/
protected $errors = array();
/**
* @todo.
*
* @var array
*/
protected $errorSections;
/**
* An array of validated forms.
*
......@@ -317,6 +303,8 @@ public function getFormStateDefaults() {
'method' => 'post',
'groups' => array(),
'buttons' => array(),
'errors' => array(),
'limit_validation_errors' => NULL,
);
}
......@@ -441,6 +429,8 @@ protected function getUncacheableKeys() {
'executed',
'validate_handlers',
'values',
'errors',
'limit_validation_errors',
);
}
......@@ -472,7 +462,7 @@ public function submitForm($form_arg, &$form_state) {
// Reset form validation.
$form_state['must_validate'] = TRUE;
$this->clearErrors();
$this->clearErrors($form_state);
$this->prepareForm($form_id, $form, $form_state);
$this->processForm($form_id, $form, $form_state);
......@@ -608,12 +598,12 @@ public function processForm($form_id, &$form, &$form_state) {
// form is processed, so scenarios that result in the form being built
// behind the scenes and again for the browser don't increment all the
// element IDs needlessly.
if (!$this->getErrors()) {
if (!$this->getAnyErrors()) {
// In case of errors, do not break HTML IDs of other forms.
$this->drupalStaticReset('drupal_html_id');
}
if ($form_state['submitted'] && !$this->getErrors() && !$form_state['rebuild']) {
if ($form_state['submitted'] && !$this->getAnyErrors() && !$form_state['rebuild']) {
// Execute form submit handlers.
$this->executeHandlers('submit', $form, $form_state);
......@@ -678,7 +668,7 @@ public function processForm($form_id, &$form, &$form_state) {
// along with element-level #submit properties, it makes no sense to
// have divergent form execution based on whether the triggering element
// has #executes_submit_callback set to TRUE.
if (($form_state['rebuild'] || !$form_state['executed']) && !$this->getErrors()) {
if (($form_state['rebuild'] || !$form_state['executed']) && !$this->getAnyErrors()) {
// Form building functions (e.g., self::handleInputElement()) may use
// $form_state['rebuild'] to determine if they are running in the
// context of a rebuild, so ensure it is set.
......@@ -852,11 +842,15 @@ public function validateForm($form_id, &$form, &$form_state) {
$url = $this->urlGenerator->generateFromPath($path, array('query' => $query));
// Setting this error will cause the form to fail validation.
$this->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
$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)));
}
}
// Recursively validate each form element.
$this->doValidateForm($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.
$this->validatedForms[$form_id] = TRUE;
// If validation errors are limited then remove any non validated form values,
......@@ -1010,7 +1004,7 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
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, $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']))));
$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'])) {
......@@ -1024,7 +1018,7 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
$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, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$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);
}
}
......@@ -1043,7 +1037,7 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
$this->setValue($elements, NULL, $form_state);
}
elseif (!isset($options[$elements['#value']])) {
$this->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
$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);
}
}
......@@ -1061,7 +1055,7 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// 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']))) {
$this->setErrorByName(NULL, '', $form_state['triggering_element']['#limit_validation_errors']);
$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
......@@ -1073,14 +1067,14 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// 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']) {
$this->setErrorByName(NULL, '', array());
$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 {
$this->errorSections = NULL;
$form_state['limit_validation_errors'] = NULL;
}
// Make sure a value is passed when the field is required.
......@@ -1120,17 +1114,17 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// 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, $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, $this->t('!name field is required.', array('!name' => $elements['#title'])));
$this->setError($elements, $form_state, $this->t('!name field is required.', array('!name' => $elements['#title'])));
}
else {
$this->setError($elements);
$this->setError($elements, $form_state);
}
}
......@@ -1140,7 +1134,30 @@ protected function doValidateForm(&$elements, &$form_state, $form_id = NULL) {
// 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.
$this->errorSections = NULL;
$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 ($this->elementChildren($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);
}
/**
......@@ -1179,14 +1196,10 @@ public function executeHandlers($type, &$form, &$form_state) {
/**
* {@inheritdoc}
*/
public function setErrorByName($name = NULL, $message = '', $limit_validation_errors = NULL) {
if (isset($limit_validation_errors)) {
$this->errorSections = $limit_validation_errors;
}
if (isset($name) && !isset($this->errors[$name])) {
public function setErrorByName($name, array &$form_state, $message = '') {
if (!isset($form_state['errors'][$name])) {
$record = TRUE;
if (isset($this->errorSections)) {
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.
......@@ -1195,7 +1208,7 @@ public function setErrorByName($name = NULL, $message = '', $limit_validation_er
// its submit action to be triggered even if none of the submitted
// values are valid.
$record = FALSE;
foreach ($this->errorSections as $section) {
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
......@@ -1210,44 +1223,51 @@ public function setErrorByName($name = NULL, $message = '', $limit_validation_er
}
}
if ($record) {
$this->errors[$name] = $message;
$form_state['errors'][$name] = $message;
$this->request->attributes->set('_form_errors', TRUE);
if ($message) {
$this->drupalSetMessage($message, 'error');
}
}
}
return $this->errors;
return $form_state['errors'];
}
/**
* {@inheritdoc}
*/
public function clearErrors() {
$this->errors = array();
public function clearErrors(array &$form_state) {
$form_state['errors'] = array();
$this->request->attributes->set('_form_errors', FALSE);
}
/**
* {@inheritdoc}
*/
public function getErrors() {
$form = $this->setErrorByName();
if (!empty($form)) {
return $form;
}
public function getErrors(array $form_state) {
return $form_state['errors'];
}
/**
* {@inheritdoc}
*/
public function getError($element) {
$form = $this->setErrorByName();
$parents = array();
foreach ($element['#parents'] as $parent) {
$parents[] = $parent;
$key = implode('][', $parents);
if (isset($form[$key])) {
return $form[$key];
public function getAnyErrors() {
return (bool) $this->request->attributes->get('_form_errors');
}
/**
* {@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];
}
}
}
}
......@@ -1255,8 +1275,8 @@ public function getError($element) {
/**
* {@inheritdoc}
*/
public function setError(&$element, $message = '') {
$this->setErrorByName(implode('][', $element['#parents']), $message);
public function setError(&$element, array &$form_state, $message = '') {
$this->setErrorByName(implode('][', $element['#parents']), $form_state, $message);
}
/**
......@@ -1277,6 +1297,7 @@ public function doBuildForm($form_id, &$element, &$form_state) {
'#required' => FALSE,
'#attributes' => array(),
'#title_display' => 'before',
'#errors' => NULL,
);
// Special handling if we're on the top level form element.
......
......@@ -12,7 +12,7 @@
/**
* Provides an interface for form building and processing.
*/
interface FormBuilderInterface {
interface FormBuilderInterface extends FormErrorInterface {
/**
* Determines the form ID.
......@@ -508,128 +508,6 @@ public function redirectForm($form_state);
*/
public function executeHandlers($type, &$form, &$form_state);