Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1173
diff -u -p -r1.1173 common.inc
--- includes/common.inc	26 May 2010 10:52:28 -0000	1.1173
+++ includes/common.inc	26 May 2010 17:15:52 -0000
@@ -6624,6 +6624,126 @@ function entity_invoke($op, $entity_type
 }
 
 /**
+ * Helper function to add entity variables and processing to entity forms.
+ *
+ * Entity editing forms require an association between the entity being edited
+ * and the form. For example, a typical workflow might involve a form being
+ * displayed for editing an existing entity loaded from the database, so the
+ * form builder needs to set element default values based on the data already in
+ * the entity. Then the user edits some of the form values and clicks the
+ * "Preview" button, requiring the entity object associated with the form to be
+ * updated for previewing, but not yet saved to the database, and the form needs
+ * to be redisplayed appropriately for the updated entity. Then the user edits
+ * some more form values and clicks "Save", requiring the entity to be updated
+ * again, and saved to the database. Throughout the workflow, the form building
+ * and processing functions need access to the associated entity.
+ *
+ * This helper function adds default variables and processing functions
+ * appropriate for most, simple single-entity forms. Advanced entity forms and
+ * forms that simultaneously edit multiple entities may need to implement their
+ * own logic instead of calling this function.
+ *
+ * We do not call field_attach_form() from this helper function, because each
+ * entity form must have control over where within the form to attach the
+ * fields.
+ *
+ * @see entity_form_process()
+ * @see entity_form_validate()
+ * @see entity_form_pre_submit()
+ */
+function entity_form(&$form, &$form_state, $entity_type, $initial_entity) {
+  // The entity will be updated by submit handlers and must persist across form
+  // rebuilds, so it needs to be part of the form state. Once set, it should not
+  // be overwritten: submit handlers can update the contents of the entity, but
+  // nothing should overwrite $form_state['entity'] itself once it is
+  // initialized.
+  if (!isset($form_state['entity'])) {
+    $form_state['entity'] = $initial_entity;
+  }
+
+  // Give form building and processing functions convenient access to the
+  // entity.
+  $form['#entity_type'] = $entity_type;
+  $form['#' . $entity_type] = $form_state['entity'];
+
+  // When the form is retrieved from cache, the object identity between
+  // $form['#' . $entity_type] and $form_state['entity'] is broken, and must
+  // be re-established in a #process function.
+  $form['#process'][] = 'entity_form_process';
+
+  // Provide default validate and pre_submit implementations appropriate for
+  // entity forms.
+  $form['#validate'][] = 'entity_form_validate';
+  $form['#pre_submit'][] = 'entity_form_pre_submit';
+}
+
+/**
+ * Re-establish identity between the entity in $form and $form_state after cache retrieval.
+ */
+function entity_form_process($form, &$form_state) {
+  $form['#' . $form['#entity_type']] = $form_state['entity'];
+  return $form;
+}
+
+/**
+ * Default validation handler for entity forms.
+ */
+function entity_form_validate($form, &$form_state) {
+  $info = entity_get_info($form['#entity_type']);
+  if ($info['fieldable']) {
+    // All field attach API functions act on an entity object, but during form
+    // validation, we don't have one. $form_state['entity'] contains the entity
+    // as it was prior to processing the current form submission, and we must
+    // not update it until we have fully validated the submitted input.
+    // Therefore, for validation, act on a pseudo entity that is simply an
+    // object representation of the data needing validation.
+    $pseudo_entity = (object) $form_state['values'];
+    field_attach_form_validate($form['#entity_type'], $pseudo_entity, $form, $form_state);
+  }
+}
+
+/**
+ * Default pre_submit handler for entity forms.
+ *
+ * After clicking any button (e.g., "Save", "Preview", "Next", etc.), if the
+ * form was fully validated (i.e., the button did not use
+ * #limit_validation_errors to limit validation to a local field), then the
+ * entity associated with the form state must be updated to reflect the
+ * submitted values.
+ *
+ * An entity form needing to customize how form values map to the entity may add
+ * pre_submit handlers that run after or instead of this one.
+ *
+ * @see entity_form()
+ */
+function entity_form_pre_submit($form, &$form_state) {
+  $entity_type = $form['#entity_type'];
+  $entity = $form_state['entity'];
+  $entity_info = entity_get_info($entity_type);
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+  // Transfer all top-level, non-field form values to entity properties without
+  // wiping entity properties not being edited by the form. Modules may
+  // implement additional #pre_submit handlers for additional form value
+  // extraction logic. Modules that add complex, non-field form elements to an
+  // entity form may need to prevent this function from running (by removing it
+  // from the form's #entity_submit property) if this top-level merging results
+  // in previously set nested data within an entity property being wiped.
+  // However, module authors should consider whether such complex data would be
+  // better implemented as a field. We do not transfer field data here, because
+  // the Field API handles that during field_attach_submit() execution.
+  $values_excluding_fields = $entity_info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values'];
+  foreach ($values_excluding_fields as $key => $value) {
+    $entity->$key = $value;
+  }
+
+  // Transfer field values to the entity.
+  if ($entity_info['fieldable']) {
+    field_attach_submit($entity_type, $entity, $form, $form_state);
+  }
+}
+
+/**
  * Performs one or more XML-RPC request(s).
  *
  * @param $url
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.464
diff -u -p -r1.464 form.inc
--- includes/form.inc	19 May 2010 19:22:24 -0000	1.464
+++ includes/form.inc	26 May 2010 17:15:53 -0000
@@ -423,6 +423,7 @@ function form_state_keys_no_cache() {
     'groups',
     'input',
     'method',
+    'pre_submit_handlers',
     'submit_handlers',
     'submitted',
     'validate_handlers',
@@ -646,6 +647,7 @@ function drupal_process_form($form_id, &
 
     if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild']) {
       // Execute form submit handlers.
+      form_execute_handlers('pre_submit', $form, $form_state);
       form_execute_handlers('submit', $form, $form_state);
 
       // We'll clear out the cached copies of the form and its stored data
@@ -761,16 +763,10 @@ function drupal_prepare_form($form_id, &
   $form += element_info('form');
   $form += array('#tree' => FALSE, '#parents' => array());
 
-  if (!isset($form['#validate'])) {
-    if (function_exists($form_id . '_validate')) {
-      $form['#validate'] = array($form_id . '_validate');
-    }
-  }
-
-  if (!isset($form['#submit'])) {
-    if (function_exists($form_id . '_submit')) {
-      // We set submit here so that it can be altered.
-      $form['#submit'] = array($form_id . '_submit');
+  // Auto-discover default form-level handlers prior to calling drupal_alter().
+  foreach (array('validate', 'pre_submit', 'submit') as $type) {
+    if (!isset($form['#' . $type]) && function_exists($form_id . '_' . $type)) {
+      $form['#' . $type] = array($form_id . '_' . $type);
     }
   }
 
@@ -1367,8 +1363,14 @@ function form_builder($form_id, $element
 
     // If the triggering element specifies "button-level" validation and submit
     // handlers to run instead of the default form-level ones, then add those to
-    // the form state.
-    foreach (array('validate', 'submit') as $type) {
+    // the form state. _form_validate() protects agains form-level submit
+    // handlers running when #limit_validation_errors is in-use. Here we add a
+    // similar protection to not run form-level pre_submit handlers without a
+    // fully validated form.
+    if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) {
+      $form_state['pre_submit_handlers'] = array();
+    }
+    foreach (array('validate', 'pre_submit', 'submit') as $type) {
       if (isset($form_state['triggering_element']['#' . $type])) {
         $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
       }
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.543
diff -u -p -r1.543 book.module
--- modules/book/book.module	23 May 2010 19:10:22 -0000	1.543
+++ modules/book/book.module	26 May 2010 17:15:53 -0000
@@ -416,15 +416,13 @@ function book_form_alter(&$form, &$form_
 
     if ($access) {
       _book_add_form_elements($form, $form_state, $node);
+      // Since the "Book" dropdown can't trigger a form submission when
+      // JavaScript is disabled, add a submit button to do that. book.css hides
+      // this button when JavaScript is enabled.
       $form['book']['pick-book'] = array(
         '#type' => 'submit',
         '#value' => t('Change book (update list of parents)'),
-         // Submit the node form so the parent select options get updated.
-         // This is typically only used when JS is disabled. Since the parent options
-         // won't be changed via AJAX, a button is provided in the node form to submit
-         // the form and generate options in the parent select corresponding to the
-         // selected book. This is similar to what happens during a node preview.
-        '#submit' => array('node_form_submit_build_node'),
+        '#submit' => array('book_pick_book_nojs_submit'),
         '#weight' => 20,
       );
     }
@@ -432,6 +430,22 @@ function book_form_alter(&$form, &$form_
 }
 
 /**
+ * Submit handler to change a node's book.
+ *
+ * This handler is run when JavaScript is disabled. It triggers the form to
+ * rebuild so that the "Parent item" options are changed to reflect the newly
+ * selected book. When JavaScript is enabled, the submit button that triggers
+ * this handler is hidden, and the "Book" dropdown directly triggers the
+ * book_form_update() AJAX callback instead.
+ *
+ * @see book_form_update()
+ */
+function book_pick_book_nojs_submit($form, &$form_state) {
+  $form['#node']->book = $form_state['values']['book'];
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
  * Build the parent selection form element for the node form or outline tab.
  *
  * This function is also called when generating a new set of options during the
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.877
diff -u -p -r1.877 comment.module
--- modules/comment/comment.module	26 May 2010 07:52:12 -0000	1.877
+++ modules/comment/comment.module	26 May 2010 17:15:54 -0000
@@ -1744,23 +1744,8 @@ function comment_edit_page($comment) {
 function comment_form($form, &$form_state, $comment) {
   global $user;
 
-  $node = node_load($comment->nid);
-  $form['#node'] = $node;
-
-  $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
-  $is_admin = (!empty($comment->cid) && user_access('administer comments'));
-
-  if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
-    $form['#attached']['library'][] = array('system', 'cookie');
-    $form['#attributes']['class'][] = 'user-info-from-cookie';
-  }
-
-  $comment = (array) $comment;
-  // Take into account multi-step rebuilding.
-  if (isset($form_state['comment'])) {
-    $comment = $form_state['comment'] + (array) $comment;
-  }
-  $comment += array(
+  // Ensure these default properties are present in the comment object.
+  $defaults = array(
     'name' => '',
     'mail' => '',
     'homepage' => '',
@@ -1771,7 +1756,27 @@ function comment_form($form, &$form_stat
     'language' => LANGUAGE_NONE,
     'uid' => 0,
   );
-  $comment = (object) $comment;
+  foreach ($defaults as $key => $value) {
+    if (!isset($comment->$key)) {
+      $comment->$key = $value;
+    }
+  }
+
+  entity_form($form, $form_state, 'comment', $comment);
+  $form['#validate'][] = 'comment_form_validate';
+  $form['#pre_submit'][] = 'comment_form_pre_submit';
+  $comment = $form['#comment'];
+
+  $node = node_load($comment->nid);
+  $form['#node'] = $node;
+
+  $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
+  $is_admin = (!empty($comment->cid) && user_access('administer comments'));
+
+  if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
+    $form['#attached']['library'][] = array('system', 'cookie');
+    $form['#attributes']['class'][] = 'user-info-from-cookie';
+  }
 
   // If not replying to a comment, use our dedicated page callback for new
   // comments on nodes.
@@ -1938,7 +1943,6 @@ function comment_form($form, &$form_stat
 
   // Attach fields.
   $comment->node_type = 'comment_node_' . $node->type;
-  $form['#builder_function'] = 'comment_form_submit_build_comment';
   field_attach_form('comment', $comment, $form, $form_state);
 
   return $form;
@@ -1948,8 +1952,8 @@ function comment_form($form, &$form_stat
  * Build a preview from submitted form values.
  */
 function comment_form_build_preview($form, &$form_state) {
-  $comment = comment_form_submit_build_comment($form, $form_state);
-  $form_state['comment_preview'] = comment_preview($comment);
+  $form_state['comment_preview'] = comment_preview($form['#comment']);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -2015,8 +2019,6 @@ function comment_preview($comment) {
  */
 function comment_form_validate($form, &$form_state) {
   global $user;
-  $comment = (object) $form_state['values'];
-  field_attach_form_validate('comment', $comment, $form, $form_state);
 
   if (!empty($form_state['values']['cid'])) {
     if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
@@ -2066,50 +2068,47 @@ function comment_form_validate($form, &$
 }
 
 /**
+ * Pre-submit handler for comment forms.
+ */
+function comment_form_pre_submit($form, &$form_state) {
+  $comment = $form['#comment'];
+  comment_submit($comment);
+}
+
+/**
  * Prepare a comment for submission.
- *
- * @param $comment
- *   An associative array containing the comment data.
  */
 function comment_submit($comment) {
-  $comment += array('subject' => '');
-  if (empty($comment['date'])) {
-    $comment['date'] = 'now';
+  // @todo Legacy support. Remove in Drupal 8.
+  if (is_array($comment)) {
+    $comment += array('subject' => '');
+    $comment = (object) $comment;
+  }
+
+  if (empty($comment->date)) {
+    $comment->date = 'now';
   }
 
-  $comment['created'] = strtotime($comment['date']);
-  $comment['changed'] = REQUEST_TIME;
+  $comment->created = strtotime($comment->date);
+  $comment->changed = REQUEST_TIME;
 
-  if (!empty($comment['name']) && ($account = user_load_by_name($comment['name']))) {
-    $comment['uid'] = $account->uid;
+  if (!empty($comment->name) && ($account = user_load_by_name($comment->name))) {
+    $comment->uid = $account->uid;
   }
 
   // Validate the comment's subject. If not specified, extract from comment body.
-  if (trim($comment['subject']) == '') {
+  if (trim($comment->subject) == '') {
     // The body may be in any format, so:
     // 1) Filter it into HTML
     // 2) Strip out all HTML tags
     // 3) Convert entities back to plain-text.
-    $comment['subject'] = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment['comment_body'][LANGUAGE_NONE][0]['value'], $comment['comment_body'][LANGUAGE_NONE][0]['format'])))), 29, TRUE);
+    $comment->subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment->comment_body[LANGUAGE_NONE][0]['value'], $comment->comment_body[LANGUAGE_NONE][0]['format'])))), 29, TRUE);
     // Edge cases where the comment body is populated only by HTML tags will
     // require a default subject.
-    if ($comment['subject'] == '') {
-      $comment['subject'] = t('(No subject)');
+    if ($comment->subject == '') {
+      $comment->subject = t('(No subject)');
     }
   }
-  return (object) $comment;
-}
-
-/**
- * Build a comment by processing form values and prepare for a form rebuild.
- */
-function comment_form_submit_build_comment($form, &$form_state) {
-  $comment = comment_submit($form_state['values']);
-
-  field_attach_submit('comment', $comment, $form, $form_state);
-
-  $form_state['comment'] = (array) $comment;
-  $form_state['rebuild'] = TRUE;
   return $comment;
 }
 
@@ -2118,7 +2117,7 @@ function comment_form_submit_build_comme
  */
 function comment_form_submit($form, &$form_state) {
   $node = node_load($form_state['values']['nid']);
-  $comment = comment_form_submit_build_comment($form, $form_state);
+  $comment = $form['#comment'];
   if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
     // Save the anonymous user information to a cookie for reuse.
     if (!$comment->uid) {
@@ -2155,7 +2154,6 @@ function comment_form_submit($form, &$fo
     // Redirect the user to the node they are commenting on.
     $redirect = 'node/' . $node->nid;
   }
-  unset($form_state['rebuild']);
   $form_state['redirect'] = $redirect;
   // Clear the block and page caches so that anonymous users see the comment
   // they have posted.
Index: modules/field/field.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.88
diff -u -p -r1.88 field.attach.inc
--- modules/field/field.attach.inc	23 May 2010 19:10:23 -0000	1.88
+++ modules/field/field.attach.inc	26 May 2010 17:15:54 -0000
@@ -556,9 +556,6 @@ function field_attach_form($entity_type,
   $form['#entity_type'] = $entity_type;
   $form['#bundle'] = $bundle;
 
-  // Save the original entity to allow later re-use.
-  $form_state['entity'] = $entity;
-
   // Let other modules make changes to the form.
   // Avoid module_invoke_all() to let parameters be taken by reference.
   foreach (module_implements('field_attach_form') as $module) {
Index: modules/field/field.default.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v
retrieving revision 1.36
diff -u -p -r1.36 field.default.inc
--- modules/field/field.default.inc	23 May 2010 19:10:23 -0000	1.36
+++ modules/field/field.default.inc	26 May 2010 17:15:54 -0000
@@ -65,25 +65,12 @@ function field_default_validate($entity_
 }
 
 function field_default_submit($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) {
-  $field_name = $field['field_name'];
-
-  if (isset($form_state['values'][$field_name][$langcode])) {
-    // Reorder items to account for drag-n-drop reordering.
-    if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
-      $items = _field_sort_items($field, $items);
-    }
-    // Filter out empty values.
-    $items = _field_filter_items($field, $items);
-  }
-  elseif (!empty($entity->revision) && isset($form_state['entity']->{$field_name}[$langcode])) {
-    // To ensure new revisions are created with all field values in all
-    // languages, populate values not included in the form with the ones from
-    // the original object. This covers:
-    // - partial forms including only a subset of the fields,
-    // - fields for which the user has no edit access,
-    // - languages not involved in the form.
-    $items = $form_state['entity']->{$field_name}[$langcode];
+  // Reorder items to account for drag-n-drop reordering.
+  if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
+    $items = _field_sort_items($field, $items);
   }
+  // Filter out empty values.
+  $items = _field_filter_items($field, $items);
 }
 
 /**
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.49
diff -u -p -r1.49 field.form.inc
--- modules/field/field.form.inc	23 May 2010 07:30:56 -0000	1.49
+++ modules/field/field.form.inc	26 May 2010 17:15:54 -0000
@@ -359,17 +359,12 @@ function field_default_form_errors($enti
  * to return just the changed part of the form.
  */
 function field_add_more_submit($form, &$form_state) {
-  // Set the form to rebuild and run submit handlers.
-  if (isset($form['#builder_function']) && function_exists($form['#builder_function'])) {
-    $entity = $form['#builder_function']($form, $form_state);
-
-    // Make the changes we want to the form state.
-    $field_name = $form_state['clicked_button']['#field_name'];
-    $langcode = $form_state['clicked_button']['#language'];
-    if ($form_state['values'][$field_name . '_add_more']) {
-      $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$langcode]);
-    }
+  $field_name = $form_state['clicked_button']['#field_name'];
+  $langcode = $form_state['clicked_button']['#language'];
+  if ($form_state['values'][$field_name . '_add_more']) {
+    $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$langcode]);
   }
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
Index: modules/field/tests/field_test.entity.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/tests/field_test.entity.inc,v
retrieving revision 1.10
diff -u -p -r1.10 field_test.entity.inc
--- modules/field/tests/field_test.entity.inc	23 May 2010 19:10:23 -0000	1.10
+++ modules/field/tests/field_test.entity.inc	26 May 2010 17:15:54 -0000
@@ -264,10 +264,8 @@ function field_test_entity_edit($entity)
  * Test_entity form.
  */
 function field_test_entity_form($form, &$form_state, $entity, $add = FALSE) {
-  if (isset($form_state['test_entity'])) {
-    $entity = $form_state['test_entity'] + (array) $entity;
-  }
-  $entity = (object) $entity;
+  entity_form($form, $form_state, 'test_entity', $entity);
+  $entity = $form['#test_entity'];
 
   foreach (array('ftid', 'ftvid', 'fttype') as $key) {
     $form[$key] = array(
@@ -277,7 +275,6 @@ function field_test_entity_form($form, &
   }
 
   // Add field widgets.
-  $form['#builder_function'] = 'field_test_entity_form_submit_builder';
   field_attach_form('test_entity', $entity, $form, $form_state);
 
   if (!$add) {
@@ -299,18 +296,10 @@ function field_test_entity_form($form, &
 }
 
 /**
- * Validate handler for field_test_entity_form().
- */
-function field_test_entity_form_validate($form, &$form_state) {
-  $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']);
-  field_attach_form_validate('test_entity', $entity, $form, $form_state);
-}
-
-/**
  * Submit handler for field_test_entity_form().
  */
 function field_test_entity_form_submit($form, &$form_state) {
-  $entity = field_test_entity_form_submit_builder($form, $form_state);
+  $entity = $form['#test_entity'];
   $insert = empty($entity->ftid);
   field_test_entity_save($entity);
 
@@ -318,25 +307,11 @@ function field_test_entity_form_submit($
   drupal_set_message($message);
 
   if ($entity->ftid) {
-    unset($form_state['rebuild']);
     $form_state['redirect'] = 'test-entity/' . $entity->ftid . '/edit';
   }
   else {
     // Error on save.
+    $form_state['rebuild'] = TRUE;
     drupal_set_message(t('The entity could not be saved.'), 'error');
   }
 }
-
-/**
- * Builds a test_entity from submitted form values.
- */
-function field_test_entity_form_submit_builder($form, &$form_state) {
-  $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']);
-  $entity->revision = !empty($form_state['values']['revision']);
-  field_attach_submit('test_entity', $entity, $form, $form_state);
-
-  $form_state['test_entity'] = (array) $entity;
-  $form_state['rebuild'] = TRUE;
-
-  return $entity;
-}
Index: modules/menu/menu.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v
retrieving revision 1.229
diff -u -p -r1.229 menu.module
--- modules/menu/menu.module	7 Mar 2010 07:55:14 -0000	1.229
+++ modules/menu/menu.module	26 May 2010 17:15:55 -0000
@@ -596,7 +596,6 @@ function menu_form_alter(&$form, $form_s
       return;
     }
     $link = $form['#node']->menu;
-    $form['#submit'][] = 'menu_node_form_submit';
 
     $form['menu'] = array(
       '#type' => 'fieldset',
@@ -661,15 +660,13 @@ function menu_form_alter(&$form, $form_s
 }
 
 /**
- * Submit handler for node form.
- *
- * @see menu_form_alter()
+ * Implements hook_node_submit().
  */
-function menu_node_form_submit($form, &$form_state) {
+function menu_node_submit($node, $form, $form_state) {
   // Decompose the selected menu parent option into 'menu_name' and 'plid', if
   // the form used the default parent selection widget.
   if (!empty($form_state['values']['menu']['parent'])) {
-    list($form_state['values']['menu']['menu_name'], $form_state['values']['menu']['plid']) = explode(':', $form_state['values']['menu']['parent']);
+    list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']);
   }
 }
 
Index: modules/node/node.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v
retrieving revision 1.68
diff -u -p -r1.68 node.api.php
--- modules/node/node.api.php	23 May 2010 19:10:23 -0000	1.68
+++ modules/node/node.api.php	26 May 2010 17:15:55 -0000
@@ -681,6 +681,36 @@ function hook_node_validate($node, $form
 }
 
 /**
+ * Maps submitted form values to a node object prior to the node being saved or previewed.
+ *
+ * This hook is invoked from node_form_pre_submit(), after the user input has
+ * been validated and mapped to the $form['#node'] object. Modules may implement
+ * this hook to perform additional custom mapping not already handled by
+ * entity_form_pre_submit() and node_submit(). The most common use of this hook
+ * is for a module that adds form elements that do not correspond to Field API
+ * fields to the node form, and needs to customize how the submitted values in
+ * those elements map to the node.
+ *
+ * @param $node
+ *   The node to update. Updates are not saved to the database until the form
+ *   submit handler calls node_save(), and may not be saved to the database if
+ *   the node is being previewed rather than saved.
+ * @param $form
+ *   The form being used to edit the node.
+ * @param $form_state
+ *   The form state array.
+ *
+ * @ingroup node_api_hooks
+ */
+function hook_node_submit($node, $form, $form_state) {
+  // Decompose the selected menu parent option into 'menu_name' and 'plid', if
+  // the form used the default parent selection widget.
+  if (!empty($form_state['values']['menu']['parent'])) {
+    list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']);
+  }
+}
+
+/**
  * Act on a node that is being assembled before rendering.
  *
  * The module may add elements to $node->content prior to rendering. This hook
Index: modules/node/node.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v
retrieving revision 1.126
diff -u -p -r1.126 node.pages.inc
--- modules/node/node.pages.inc	10 May 2010 06:34:39 -0000	1.126
+++ modules/node/node.pages.inc	26 May 2010 17:15:55 -0000
@@ -75,13 +75,17 @@ function node_add($type) {
   return $output;
 }
 
+/**
+ * Validation handler for node forms.
+ */
 function node_form_validate($form, &$form_state) {
+  // See entity_form_validate() for why we use a pseudo entity during
+  // validation.
   $node = (object) $form_state['values'];
-  node_validate($node, $form);
 
-  // Field validation. Requires access to $form_state, so this cannot be
-  // done in node_validate() as it currently exists.
-  field_attach_form_validate('node', $node, $form, $form_state);
+  // @todo $form_state is not passed to node_validate() for legacy reasons only.
+  //   Should this be changed for Drupal 7 or wait until Drupal 8?
+  node_validate($node, $form);
 }
 
 /**
@@ -89,14 +93,15 @@ function node_form_validate($form, &$for
  */
 function node_form($form, &$form_state, $node) {
   global $user;
-  // This form has its own multistep persistence.
-  if ($form_state['rebuild']) {
-    $form_state['input'] = array();
-  }
 
-  if (isset($form_state['node'])) {
-    $node = (object) ($form_state['node'] + (array) $node);
-  }
+  // In addition to the validation and pre_submit handlers common to all entity
+  // forms, we want to add handlers specific to the node form.
+  $form['#validate'][] = 'node_form_validate';
+  $form['#pre_submit'][] = 'node_form_pre_submit_invoke_legacy_handlers';
+  entity_form($form, $form_state, 'node', $node);
+  $form['#pre_submit'][] = 'node_form_pre_submit';
+  $node = $form['#node'];
+
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -141,8 +146,6 @@ function node_form($form, &$form_state, 
     $form['title']['#weight'] = -5;
   }
 
-  $form['#node'] = $node;
-
   $form['additional_settings'] = array(
     '#type' => 'vertical_tabs',
     '#weight' => 99,
@@ -276,9 +279,7 @@ function node_form($form, &$form_state, 
       '#submit' => array('node_form_delete_submit'),
     );
   }
-  $form['#validate'][] = 'node_form_validate';
 
-  $form['#builder_function'] = 'node_form_submit_build_node';
   field_attach_form('node', $node, $form, $form_state, $node->language);
 
   return $form;
@@ -299,8 +300,8 @@ function node_form_delete_submit($form, 
 
 
 function node_form_build_preview($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
-  $form_state['node_preview'] = node_preview($node);
+  $form_state['node_preview'] = node_preview($form['#node']);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -382,7 +383,7 @@ function theme_node_preview($variables) 
 }
 
 function node_form_submit($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = $form['#node'];
   $insert = empty($node->nid);
   node_save($node);
   $node_link = l(t('view'), 'node/' . $node->nid);
@@ -398,7 +399,6 @@ function node_form_submit($form, &$form_
     drupal_set_message(t('@type %title has been updated.', $t_args));
   }
   if ($node->nid) {
-    unset($form_state['rebuild']);
     $form_state['values']['nid'] = $node->nid;
     $form_state['nid'] = $node->nid;
     $form_state['redirect'] = 'node/' . $node->nid;
@@ -407,26 +407,37 @@ function node_form_submit($form, &$form_
     // In the unlikely case something went wrong on save, the node will be
     // rebuilt and node form redisplayed the same way as in preview.
     drupal_set_message(t('The post could not be saved.'), 'error');
+    $form_state['rebuild'] = TRUE;
   }
   // Clear the page and block caches.
   cache_clear_all();
 }
 
 /**
- * Build a node by processing submitted form values and prepare for a form rebuild.
+ * Pre-submit handler for node forms that runs before entity_form_pre_submit().
  */
-function node_form_submit_build_node($form, &$form_state) {
-  // Unset any button-level handlers, execute all the form-level submit
-  // functions to process the form values into an updated node.
-  unset($form_state['submit_handlers']);
-  form_execute_handlers('submit', $form, $form_state);
-  $node = node_submit((object) $form_state['values']);
-
-  field_attach_submit('node', $node, $form, $form_state);
+function node_form_pre_submit_invoke_legacy_handlers($form, &$form_state) {
+  // @todo Legacy support for modules that extend the node form with form-level
+  //   submit handlers that must run before button-level submit handlers.
+  //   Remove this in Drupal 8. Module authors are encouraged to convert to
+  //   using pre_submit handlers or hook_node_submit().
+  if (isset($form['#submit']) && isset($form_state['submit_handlers'])) {
+    foreach ($form['#submit'] as $function) {
+      $function($form, $form_state);
+    }
+  }
+}
 
-  $form_state['node'] = (array) $node;
-  $form_state['rebuild'] = TRUE;
-  return $node;
+/**
+ * Pre-submit handler for node forms.
+ */
+function node_form_pre_submit($form, &$form_state) {
+  $node = $form['#node'];
+  node_submit($node);
+  foreach (module_implements('node_submit') as $module) {
+    $function = $module . '_node_submit';
+    $function($node, $form, $form_state);
+  }
 }
 
 /**
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.349
diff -u -p -r1.349 poll.module
--- modules/poll/poll.module	23 May 2010 19:10:23 -0000	1.349
+++ modules/poll/poll.module	26 May 2010 17:15:55 -0000
@@ -366,15 +366,18 @@ function poll_form($node, &$form_state) 
  * return just the changed part of the form.
  */
 function poll_more_choices_submit($form, &$form_state) {
-  include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc';
-  // Set the form to rebuild and run submit handlers.
-  node_form_submit_build_node($form, $form_state);
-
   // Make the changes we want to the form state.
   if ($form_state['values']['poll_more']) {
     $n = $_GET['q'] == 'system/ajax' ? 1 : 5;
     $form_state['choice_count'] = count($form_state['values']['choice']) + $n;
   }
+  // Renumber the choices. This invalidates the corresponding key/value
+  // associations in $form_state['input'], so clear that out. This requires
+  // poll_form() to rebuild the choices with the values in
+  // $form['#node']->choice, which it does.
+  $form['#node']->choice = array_values($form_state['values']['choice']);
+  unset($form_state['input']['choice']);
+  $form_state['rebuild'] = TRUE;
 }
 
 function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight = 0, $size = 10) {
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.39
diff -u -p -r1.39 form_test.module
--- modules/simpletest/tests/form_test.module	28 Apr 2010 16:11:22 -0000	1.39
+++ modules/simpletest/tests/form_test.module	26 May 2010 17:15:56 -0000
@@ -1106,8 +1106,6 @@ function form_test_form_user_register_fo
   if (!empty($_REQUEST['field'])) {
     $node = (object)array('type' => 'page');
     field_attach_form('node', $node, $form, $form_state);
-    // The form API requires the builder function to set rebuilding, so do so.
-    $form['#builder_function'] = 'form_test_user_register_form_rebuild';
   }
 }
 
Index: modules/taxonomy/taxonomy.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.admin.inc,v
retrieving revision 1.105
diff -u -p -r1.105 taxonomy.admin.inc
--- modules/taxonomy/taxonomy.admin.inc	13 May 2010 07:53:02 -0000	1.105
+++ modules/taxonomy/taxonomy.admin.inc	26 May 2010 17:15:56 -0000
@@ -630,20 +630,19 @@ function taxonomy_form_term($form, &$for
     'weight' => 0,
   );
 
-  // Take into account multi-step rebuilding.
-  if (isset($form_state['term'])) {
-    $edit = $form_state['term'] + $edit;
-  }
+  entity_form($form, $form_state, 'taxonomy_term', (object) $edit);
+  $form['#validate'][] = 'taxonomy_form_term_validate';
+  $form['#pre_submit'][] = 'taxonomy_form_term_pre_submit';
+  $term = $form['#taxonomy_term'];
 
-  $parent = array_keys(taxonomy_get_parents($edit['tid']));
-  $form['#term'] = $edit;
+  $parent = array_keys(taxonomy_get_parents($term->tid));
+  $form['#term'] = (array) $term;
   $form['#term']['parent'] = $parent;
   $form['#vocabulary'] = $vocabulary;
-  $form['#builder_function'] = 'taxonomy_form_term_submit_builder';
 
   // Check for confirmation forms.
   if (isset($form_state['confirm_delete'])) {
-    return array_merge($form, taxonomy_term_confirm_delete($form, $form_state, $edit['tid']));
+    return array_merge($form, taxonomy_term_confirm_delete($form, $form_state, $term->tid));
   }
   elseif (isset($form_state['confirm_parents'])) {
     return array_merge($form, taxonomy_term_confirm_parents($form, $form_state, $vocabulary));
@@ -652,7 +651,7 @@ function taxonomy_form_term($form, &$for
   $form['name'] = array(
     '#type' => 'textfield',
     '#title' => t('Name'),
-    '#default_value' => $edit['name'],
+    '#default_value' => $term->name,
     '#maxlength' => 255,
     '#required' => TRUE,
     '#weight' => -5,
@@ -660,18 +659,18 @@ function taxonomy_form_term($form, &$for
   $form['description'] = array(
     '#type' => 'text_format',
     '#title' => t('Description'),
-    '#default_value' => $edit['description'],
-    '#format' => $edit['format'],
+    '#default_value' => $term->description,
+    '#format' => $term->format,
     '#weight' => 0,
   );
 
   $form['vocabulary_machine_name'] = array(
     '#type' => 'textfield',
     '#access' => FALSE,
-    '#value' => isset($edit['vocabulary_machine_name']) ? $edit['vocabulary_machine_name'] : $vocabulary->name,
+    '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name,
   );
 
-  field_attach_form('taxonomy_term', (object) $edit, $form, $form_state);
+  field_attach_form('taxonomy_term', $term, $form, $form_state);
 
   $form['relations'] = array(
     '#type' => 'fieldset',
@@ -686,23 +685,23 @@ function taxonomy_form_term($form, &$for
   // full vocabulary. Contrib modules can then intercept before
   // hook_form_alter to provide scalable alternatives.
   if (!variable_get('taxonomy_override_selector', FALSE)) {
-    $parent = array_keys(taxonomy_get_parents($edit['tid']));
-    $children = taxonomy_get_tree($vocabulary->vid, $edit['tid']);
+    $parent = array_keys(taxonomy_get_parents($term->tid));
+    $children = taxonomy_get_tree($vocabulary->vid, $term->tid);
 
     // A term can't be the child of itself, nor of its children.
     foreach ($children as $child) {
       $exclude[] = $child->tid;
     }
-    $exclude[] = $edit['tid'];
+    $exclude[] = $term->tid;
 
     $tree = taxonomy_get_tree($vocabulary->vid);
     $options = array('<' . t('root') . '>');
     if (empty($parent)) {
       $parent = array(0);
     }
-    foreach ($tree as $term) {
-      if (!in_array($term->tid, $exclude)) {
-        $options[$term->tid] = str_repeat('-', $term->depth) . $term->name;
+    foreach ($tree as $item) {
+      if (!in_array($item->tid, $exclude)) {
+        $options[$item->tid] = str_repeat('-', $item->depth) . $item->name;
       }
     }
     $form['relations']['parent'] = array(
@@ -718,7 +717,7 @@ function taxonomy_form_term($form, &$for
     '#type' => 'textfield',
     '#title' => t('Weight'),
     '#size' => 6,
-    '#default_value' => $edit['weight'],
+    '#default_value' => $term->weight,
     '#description' => t('Terms are displayed in ascending order by weight.'),
     '#required' => TRUE,
   );
@@ -728,7 +727,7 @@ function taxonomy_form_term($form, &$for
   );
   $form['tid'] = array(
     '#type' => 'value',
-    '#value' => $edit['tid'],
+    '#value' => $term->tid,
   );
 
   $form['actions'] = array('#type' => 'actions');
@@ -738,7 +737,7 @@ function taxonomy_form_term($form, &$for
     '#weight' => 5,
   );
 
-  if ($edit['tid']) {
+  if ($term->tid) {
     $form['actions']['delete'] = array(
       '#type' => 'submit',
       '#value' => t('Delete'),
@@ -759,8 +758,6 @@ function taxonomy_form_term($form, &$for
  * @see taxonomy_form_term()
  */
 function taxonomy_form_term_validate($form, &$form_state) {
-  field_attach_form_validate('taxonomy_term', (object) $form_state['values'], $form, $form_state);
-
   // Ensure numeric values.
   if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) {
     form_set_error('weight', t('Weight value must be numeric.'));
@@ -790,7 +787,7 @@ function taxonomy_form_term_submit($form
     return;
   }
 
-  $term = taxonomy_form_term_submit_builder($form, $form_state);
+  $term = $form['#taxonomy_term'];
 
   $status = taxonomy_term_save($term);
   switch ($status) {
@@ -828,27 +825,18 @@ function taxonomy_form_term_submit($form
 
   $form_state['values']['tid'] = $term->tid;
   $form_state['tid'] = $term->tid;
-  // Do not rebuild here. The term is saved by now and the form should clear.
-  $form_state['rebuild'] = FALSE;
 }
 
 /**
- * Build a term by processing form values and prepare for a form rebuild.
+ * Pre-submit callback for taxonomy term form.
  */
-function taxonomy_form_term_submit_builder($form, &$form_state) {
-  $term = (object) $form_state['values'];
-
+function taxonomy_form_term_pre_submit($form, &$form_state) {
   // Convert text_format field into values expected by taxonomy_term_save().
-  $description = $form_state['values']['description'];
-  $term->description = $description['value'];
-  $term->format = $description['format'];
-
-  field_attach_submit('taxonomy_term', $term, $form, $form_state);
-
-  $form_state['term'] = (array) $term;
-  $form_state['rebuild'] = TRUE;
-
-  return $term;
+  if (!empty($form_state['values']['description'])) {
+    $term = $form['#taxonomy_term'];
+    $term->description = $form_state['values']['description']['value'];
+    $term->format = $form_state['values']['description']['format'];
+  }
 }
 
 /**
