Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.324
diff -w -u -p -r1.324 form.inc
--- includes/form.inc	14 Mar 2009 20:13:26 -0000	1.324
+++ includes/form.inc	26 Mar 2009 17:22:48 -0000
@@ -1665,7 +1665,8 @@ function form_process_password_confirm($
   );
   $element['#element_validate'] = array('password_confirm_validate');
   $element['#tree'] = TRUE;
-
+   unset($element['#theme']);
+   $element['#type'] .= '_confirm';
   if (isset($element['#size'])) {
     $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size'];
   }
@@ -2023,6 +2024,112 @@ function form_process_ahah($element) {
 }
 
 /**
+ * If the confirm property is set, do several things:
+ * * Expand the field into two matching fields
+ * * Add form validation and submit handlers to ensure fields match
+ * * Set up UI feedback
+ * 
+ * Note: A #confirm element with an #ahah property should define its own
+ * UI feedback callback, as such a property will override the default.
+ *
+ * @param $element
+ *		 An associative array containing the properties of the element.
+ *		 Properties used: confirm, title, default_value
+ * @return
+ *		 None. 
+ * @see theme_confirm_field
+ *	 Adds code to the header of the page using drupal_add_js.
+ * @TODO
+ *		 Identify and implement any additional #confirm options, 
+ *		 e.g. if the confirm field should have a title other than 'Confirm '.$element['#title']
+ */
+function form_process_confirm($element) {
+  if (empty($element['#confirm'])) { return $element; }
+  drupal_add_js('misc/form.js');
+  drupal_add_js(array(
+    'confirmFields' => array(
+      $element['#id'] => array(
+      	'confirmTitle' => t($element['#title'] . ' match:'),
+        'confirmSuccess' => t('yes'),
+        'confirmFailure' => t('no'),
+    ))), 'setting');
+
+  $orig_attr = $conf_attr = isset($element['#attributes']) 
+  	 ? $element['#attributes'] : array('class' => '');
+
+  $orig_attr['class'] = isset($orig_attr['class'])
+  	 ? $orig_attr['class'] . ' confirm-original-field'
+  	 : 'confirm-original-field';
+
+  $conf_attr['class'] = isset($conf_attr['class'])
+  	 ? $conf_attr['class'] . ' confirm-confirm-field'
+  	 : 'confirm-confirm-field';
+
+  $value = empty($element['#value']) ? NULL : $element['#value'];
+
+  $element['original'] = array(
+    '#type' => $element['#type'],
+    '#title' => t($element['#title']), 
+    '#value' => is_array($value)
+      ? (isset($value['original']) ? $value['original'] : NULL) 
+      : $value,
+    '#required' => $element['#required'],
+    '#attributes' => $orig_attr,
+    //this is ok because $element['#id'] won't be rendered in the DOM
+    '#id' => $element['#id'],
+    );
+  $element['confirm'] = array(
+    '#type' => $element['#type'],
+    '#title' => t('Confirm '.$element['#title']),
+    '#value' => is_array($value)
+      ? (isset($value['confirm']) ? $value['confirm'] : NULL) 
+      : $value,
+    '#required' => $element['#required'],
+    '#attributes' => $conf_attr,
+  	 );
+  	 
+  // We need to fix the following properties on $element 
+  // so that FAPI no longer treats this as a typical field.
+  $element['#element_validate'] = array('confirm_field_validate');
+  $element['#tree'] = TRUE;
+  $element['#validate'] = FALSE;
+  $element['#title'] = FALSE;
+  // Set #id to make sure FAPI doesn't bust us for a conflicting DOM id.
+  $element['#id'] = 'rand'.rand();
+  unset($element['#theme'], $element['#type'], $element['#maxlength']);
+
+  if (isset($element['#size'])) {
+  	 $element['original']['#size'] = $element['confirm']['#size'] = $element['#size'];
+  }
+  return $element;
+}
+
+/**
+ * Validate element with #confirm property.
+ */
+function confirm_field_validate($element, &$form_state) {
+  $original = trim($element['original']['#value']);
+  if (!empty($original)) {
+    $confirm = trim($element['confirm']['#value']);
+    if ($confirm != $original) {
+      form_error($element, t($element['original']['#title'] . ' values do not match.'));
+    }
+  } elseif ($element['#required'] && !empty($form_state['input'])) {
+    form_error($element, t($element['original']['#title'] . ' field is required.'));
+  }
+
+  // Confirm field must be converted from a two-element array into a single
+  // string regardless of validation results.
+  form_set_value($element['original'], NULL, $form_state);
+  form_set_value($element['confirm'], NULL, $form_state);
+  form_set_value($element, $original, $form_state);
+
+  return $element;
+
+}
+
+
+/**
  * Format a checkbox.
  *
  * @param $element
Index: misc/form.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/form.js,v
retrieving revision 1.5
diff -w -u -p -r1.5 form.js
--- misc/form.js	13 Mar 2009 23:15:08 -0000	1.5
+++ misc/form.js	26 Mar 2009 17:22:48 -0000
@@ -31,4 +31,55 @@ Drupal.behaviors.filterGuidelines = {
   }
 };
 
+Drupal.behaviors.confirmFields = {
+  attach: function(context, settings) {
+    var translate = Drupal.settings.confirmFields;
+    $("input.confirm-original-field:not(.confirm-processed)", context).each(function() {
+      var originalInput = $(this).addClass('confirm-processed');
+      var innerWrapper = $(this).parent();
+      var outerWrapper = $(this).parent().parent();
+
+      // Add the confirmation layer.
+      var domid = $(this).attr("id");
+      var title = translate[domid]["confirmTitle"];
+      var confirmationContainer = 
+        '<div class="confirm-container">' + title + ' <span></span></div>';
+      $("input.confirm-confirm-field", outerWrapper).after(confirmationContainer).parent().addClass("confirm-parent");
+      var confirmInput = $("input.confirm-confirm-field", outerWrapper);
+      var confirmResult = $("div.confirm-container", outerWrapper);
+      var confirmChild = $("span", confirmResult);
+
+      // Check that original field and confirmation inputs match. Lifted from user.js
+      var confirmFieldCheckMatch = function () {
+        if (confirmInput.val()) {
+          var success = originalInput.val() === confirmInput.val();
+
+          // Show the confirm result.
+          confirmResult.css({ visibility: "visible" });
+
+          // Remove the previous styling if any exists.
+          if (this.confirmClass) {
+            confirmChild.removeClass(this.confirmClass);
+          }
+
+          // Fill in the success message and set the class accordingly.
+          var confirmClass = success ? "ok" : 'error';
+          var val = "confirm" + (success ? "Success" : "Failure");
+          var msg = translate[domid][val];
+          confirmChild.html(msg).addClass(confirmClass);
+          this.confirmClass = confirmClass;
+        }
+        else {
+          confirmResult.css({ visibility: "hidden" });
+        }
+      };
+
+      // Monitor keyup and blur events.
+      // Blur must be used because a mouse paste does not trigger keyup.
+      confirmInput.keyup(confirmFieldCheckMatch).blur(confirmFieldCheckMatch);
+    });
+  }
+};
+
+
 })(jQuery);
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.5
diff -w -u -p -r1.5 form.test
--- modules/simpletest/tests/form.test	17 Mar 2009 23:26:33 -0000	1.5
+++ modules/simpletest/tests/form.test	26 Mar 2009 17:22:50 -0000
@@ -75,6 +75,103 @@ class FormsTestCase extends DrupalWebTes
 }
 
 /**
+ * Check that the #confirm property creates and validates a confirm field
+ *
+ * Must meet the following criteria to pass the test:
+ * 1. Two fields show up on the form: original field and confirm field
+ * 2. Different values entered in the fields triggers an error
+ * 3. Same values in the fields does not trigger an error
+ * 
+ * * TODO: Tests for JavaScript "values match" user feedback
+**/
+class FormsTestConfirmCase extends DrupalWebTestCase {
+  
+  function getInfo() {
+    return array(
+      'name' => t('Confirm field validation'),
+      'description' => t('Validate that a field with #confirm property is confirming and validating properly.'),
+      'group' => t('Form API'),
+      );
+  }
+  
+  /**
+   * Tests #1 above.
+  **/
+  function testConfirmFieldsAppear() {
+    $form_id = $this->randomName();
+    $form = array();
+    $form['test1'] = array(
+      '#title' => $this->randomName(), 
+      '#type' => 'textfield', 
+      '#confirm' => TRUE,
+    );
+    $form_state = form_state_defaults();
+    
+    // Test that original and confirm fields appear in the form array on render.
+    drupal_prepare_form($form_id, $form, $form_state);
+    drupal_process_form($form_id, $form, $form_state);
+    $this->assertTrue(isset($form['test1']['original']) && isset($form['test1']['confirm']), 'Confirm fields added to form.');
+  }
+  
+  /**
+   * Tests #2 above.
+  **/
+  function testConfirmFieldsFail() {
+    // Test that not-equal values trigger a form error on submit.
+    $form_id = $this->randomName();
+    $form = array();
+    $form['op'] = array('#type' => 'submit', '#value' => t('Submit'));
+    $form['test2'] = array(
+      '#title' => $this->randomName(),
+      '#type' => 'textfield',
+      '#confirm' => TRUE,
+    );
+    $form_state = form_state_defaults();
+    $form_state['values']['test2'] = $this->randomName();
+    $form_state['input'] = array('test2' => array(
+      'original' => $form_state['values']['test2'],
+      'confirm' => $form_state['values']['test2'] . $this->randomName()
+      ));
+    $form_state['input']['form_id'] = $form_id;
+    $form_state['method'] = 'post';
+    drupal_prepare_form($form_id, $form, $form_state);
+    drupal_process_form($form_id, $form, $form_state);
+    $errors = form_get_errors();
+    $this->assertTrue(isset($errors['test2']), 'Conflicting confirm values trigger an error');
+    drupal_get_messages();
+  }
+  
+  /**
+   * Tests #3 above.
+  **/
+  function testConfirmFieldsPass() {
+    // Test that equal values do not trigger a form error
+    // and that the value of the original field is set properly.
+    $form_id = $this->randomName();
+    $form = array();
+    $form['op'] = array('#type' => 'submit', '#value' => t('Submit'));
+    $form['test3'] = array(
+      '#title' => $this->randomName(), 
+      '#type' => 'textfield', 
+      '#confirm' => TRUE,
+    );
+    $form_state = form_state_defaults();
+    $form_state['values']['test3'] = $this->randomName();
+    $form_state['input'] = array('test3' => array(
+      'original' => $form_state['values']['test3'],
+      'confirm' => $form_state['values']['test3'],
+      ));
+    $form_state['input']['form_id'] = $form_id;
+    $form_state['method'] = 'post';
+    drupal_prepare_form($form_id, $form, $form_state);
+    drupal_process_form($form_id, $form, $form_state);
+    $errors = form_get_errors();
+    $this->assertFalse(isset($errors['test3']), 'Matching confirm values do not trigger an error');
+    drupal_get_messages();
+  }
+}
+
+/**
  * Test form type functions for expected behavior.
  */
 class FormsTestTypeCase extends DrupalWebTestCase {
Index: modules/system/system.css
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.css,v
retrieving revision 1.53
diff -w -u -p -r1.53 system.css
--- modules/system/system.css	5 Dec 2008 12:50:28 -0000	1.53
+++ modules/system/system.css	26 Mar 2009 17:22:51 -0000
@@ -556,6 +556,6 @@ div.password-suggestions ul {
 .confirm-parent {
   margin: 0;
 }
-div.password-confirm {
+div.password-confirm, div.confirm-container {
   visibility: hidden;
 }
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.673
diff -w -u -p -r1.673 system.module
--- modules/system/system.module	17 Mar 2009 15:26:29 -0000	1.673
+++ modules/system/system.module	26 Mar 2009 17:22:53 -0000
@@ -288,7 +288,7 @@ function system_elements() {
     '#size' => 60,
     '#maxlength' => 128,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_text_format', 'form_process_ahah'),
+    '#process' => array('form_process_text_format', 'form_process_ahah', 'form_process_confirm'),
     '#theme' => 'textfield',
     '#theme_wrapper' => 'form_element',
   );
@@ -297,7 +297,7 @@ function system_elements() {
     '#input' => TRUE,
     '#size' => 60,
     '#maxlength' => 128,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('form_process_ahah', 'form_process_confirm'),
     '#theme' => 'password',
     '#theme_wrapper' => 'form_element',
   );
Index: modules/user/user.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v
retrieving revision 1.40
diff -w -u -p -r1.40 user.admin.inc
--- modules/user/user.admin.inc	26 Feb 2009 07:30:28 -0000	1.40
+++ modules/user/user.admin.inc	26 Mar 2009 17:22:53 -0000
@@ -237,7 +237,8 @@ function user_admin_settings() {
   // User registration settings.
   $form['registration'] = array('#type' => 'fieldset', '#title' => t('User registration settings'));
   $form['registration']['user_register'] = array('#type' => 'radios', '#title' => t('Public registrations'), '#default_value' => variable_get('user_register', 1), '#options' => array(t('Only site administrators can create new user accounts.'), t('Visitors can create accounts and no administrator approval is required.'), t('Visitors can create accounts but administrator approval is required.')));
-  $form['registration']['user_email_verification'] = array('#type' => 'checkbox', '#title' => t('Require e-mail verification when a visitor creates an account'), '#default_value' => variable_get('user_email_verification', TRUE), '#description' => t('If this box is checked, new users will be required to validate their e-mail address prior to logging into the site, and will be assigned a system-generated password. With it unchecked, users will be logged in immediately upon registering, and may select their own passwords during registration.'));
+  $form['registration']['user_confirm_email_on_register'] = array('#type' => 'checkbox', '#title' => t('Use an e-mail confirmation field on user registration form.'), '#default_value' => variable_get('user_confirm_email_on_register', FALSE), '#description' => t('If this box is checked, a "Confirm Email" text box will be added to the user registration form. The registrant will be required to enter their email address twice when registering to prevent typos.'));
+  $form['registration']['user_email_verification'] = array('#type' => 'checkbox', '#title' => t('Require e-mail verification when a visitor creates an account.'), '#default_value' => variable_get('user_email_verification', TRUE), '#description' => t('If this box is checked, new users will be required to validate their e-mail address prior to logging into the site, and will be assigned a system-generated password. With it unchecked, users will be logged in immediately upon registering, and may select their own passwords during registration.'));
   $form['registration']['user_registration_help'] = array('#type' => 'textarea', '#title' => t('User registration guidelines'), '#default_value' => variable_get('user_registration_help', ''), '#description' => t('This text is displayed at the top of the user registration form and is useful for helping or instructing your users.'));
 
   // Account cancellation settings.
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.970
diff -w -u -p -r1.970 user.module
--- modules/user/user.module	17 Mar 2009 02:07:42 -0000	1.970
+++ modules/user/user.module	26 Mar 2009 17:22:54 -0000
@@ -1795,6 +1795,8 @@ function user_edit_form(&$form_state, $u
     '#maxlength' => EMAIL_MAX_LENGTH,
     '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
     '#required' => TRUE,
+    // see admin/user/settings, or user.admin.inc::user_admin_settings
+    '#confirm' => variable_get('user_confirm_email_on_register', FALSE),
   );
   if (!$register) {
     $form['account']['pass'] = array('#type' => 'password_confirm',
