diff --git a/includes/form.inc b/includes/form.inc index e749239..63eb4c2 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -3364,6 +3364,30 @@ function form_process_actions($element, &$form_state) { } /** + * Processes a form button element. + * + * @param $element + * An associative array containing the properties and children of the + * form button. + * @param $form_state + * The $form_state array for the form this element belongs to. + * + * @return + * The processed element. + */ +function form_process_button($element, &$form_state) { + // We normally want to add drupal.form so that the double submit protection + // can be added to the site, however, with the addition of + // javascript_always_use_jquery, this would make most pages with a login + // block or a search form have jquery always added, changing what people who + // enabled javascript_always_use_jquery would have expected. + if (variable_get('javascript_always_use_jquery', TRUE)) { + $element['#attached']['library'][] = array('system', 'drupal.form'); + } + return $element; +} + +/** * Processes a container element. * * @param $element diff --git a/misc/form.js b/misc/form.js index 259b84e..c202c2a 100644 --- a/misc/form.js +++ b/misc/form.js @@ -58,6 +58,64 @@ Drupal.behaviors.formUpdated = { }; /** + * Prevents consecutive form submissions of identical form values. + * + * Repetitive form submissions that would submit the identical form values are + * prevented, unless the form values are different from the previously + * submitted values. + * + * This is a simplified re-implementation of a user-agent behavior that should + * be natively supported by major web browsers, but at this time, only Firefox + * has a built-in protection. + * + * A form value-based approach ensures that the constraint is triggered for + * consecutive, identical form submissions only. Compared to that, a form + * button-based approach would (1) rely on [visible] buttons to exist where + * technically not required and (2) require more complex state management if + * there are multiple buttons in a form. + * + * This implementation is based on form-level submit events only and relies on + * jQuery's serialize() method to determine submitted form values. As such, the + * following limitations exist: + * + * - Event handlers on form buttons that preventDefault() do not receive a + * double-submit protection. That is deemed to be fine, since such button + * events typically trigger reversible client-side or server-side operations + * that are local to the context of a form only. + * - Changed values in advanced form controls, such as file inputs, are not part + * of the form values being compared between consecutive form submits (due to + * limitations of jQuery.serialize()). That is deemed to be acceptable, + * because if the user forgot to attach a file, then the size of HTTP payload + * will most likely be small enough to be fully passed to the server endpoint + * within (milli)seconds. If a user mistakenly attached a wrong file and is + * technically versed enough to cancel the form submission (and HTTP payload) + * in order to attach a different file, then that edge-case is not supported + * here. + * + * Lastly, all forms submitted via HTTP GET are idempotent by definition of HTTP + * standards, so excluded in this implementation. + */ +Drupal.behaviors.formSingleSubmit = { + attach: function () { + function onFormSubmit (e) { + var $form = $(e.currentTarget); + var formValues = $form.serialize(); + var previousValues = $form.attr('data-drupal-form-submit-last'); + if (previousValues === formValues) { + e.preventDefault(); + } + else { + $form.attr('data-drupal-form-submit-last', formValues); + } + } + + $('body').once('form-single-submit') + .delegate('form:not([method~="GET"])', 'submit.singleSubmit', onFormSubmit); + + } +}; + +/** * Prepopulate form fields with information from the visitor cookie. */ Drupal.behaviors.fillUserInfoFromCookie = { diff --git a/modules/system/system.module b/modules/system/system.module index 53844d8..4ea6984 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -331,7 +331,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => TRUE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#theme_wrappers' => array('button'), ); $types['button'] = array( @@ -340,7 +340,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => FALSE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#theme_wrappers' => array('button'), ); $types['image_button'] = array( @@ -348,7 +348,7 @@ function system_element_info() { '#button_type' => 'submit', '#executes_submit_callback' => TRUE, '#limit_validation_errors' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('ajax_process_form', 'form_process_button'), '#return_value' => TRUE, '#has_garbage_value' => TRUE, '#src' => NULL,