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..f7c4c15 100644
--- a/misc/form.js
+++ b/misc/form.js
@@ -58,6 +58,68 @@ 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) {
+      if (e.isDefaultPrevented()) {
+        // Don't act on form submissions that have been prevented by other JS.
+        return;
+      }
+      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,
