Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1169
diff -u -p -r1.1169 common.inc
--- includes/common.inc	20 May 2010 08:51:24 -0000	1.1169
+++ includes/common.inc	20 May 2010 21:49:51 -0000
@@ -6624,6 +6624,90 @@ function entity_invoke($op, $entity_type
 }
 
 /**
+ * Called from the form builder of an entity form.
+ */
+function entity_form_initialize($form, &$form_state, $entity_type, $entity) {
+  if (!isset($form_state['entity'])) {
+    $form_state['entity_type'] = $entity_type;
+    $form_state['entity'] = $entity;
+  }
+  return $form_state['entity'];
+}
+
+/**
+ * Update the entity being edited in response to a form submission.
+ *
+ * This function is called by submit handlers of buttons on an entity editing
+ * form that require $form_state['entity'] to be updated based on the submitted
+ * form values. For example, the submit handler of the "Save" button requires an
+ * updated entity to save to the database, and the submit handler of the
+ * "Preview" button requires an updated entity to preview. Other modules may add
+ * additional buttons with submit handlers that similarly require an updated
+ * entity.
+ *
+ * This function invokes callbacks that may assume a fully validated form, so
+ * do not call this function from button-level submit handlers of buttons that
+ * use #limit_validation_errors. Such submit handlers need to either update the
+ * entity themselves with data that has been validated (see
+ * poll_more_choices_submit()) or not update the entity at all if the form can
+ * be successfully rebuilt using the Form APIs default mechanism of retaining
+ * user input during a rebuild (see field_add_more_submit()).
+ *
+ * @param $form
+ *   The form being submitted.
+ * @param $form_state
+ *   The form state array. $form_state['entity'] contains the entity that needs
+ *   to be updated.
+ */
+function entity_form_submit($form, &$form_state) {
+  // @todo Legacy support for modules that extend the node edit form with
+  //   form-level submit handlers that need to update $form_state prior to the
+  //   entity being updated. For the entity to be updated properly during a
+  //   multi-step workflow, we must call these handlers even when this function
+  //   is called from a button-level submit handler (e.g.,
+  //   node_form_build_preview()). Module authors are encouraged to convert to
+  //   using hook_entity_submit(). Remove this in Drupal 8. Only the node edit
+  //   form requires this backwards compatibility.
+  if (!empty($form['#node_edit_form'])) {
+    unset($form_state['submit_handlers']);
+    form_execute_handlers('submit', $form, $form_state);
+  }
+
+  // Get information about the entity being submitted.
+  $entity_type = $form_state['entity_type'];
+  $entity = $form_state['entity'];
+  $entity_info = entity_get_info($entity_type);
+
+  // The entity may contain properties not being edited by the form, and these
+  // must not be wiped. For anything that is being edited by the form, update
+  // the entity with the new values.
+  foreach ($form_state['values'] as $key => $value) {
+    $entity->$key = $value;
+  }
+
+  // Invoke the entity submit callback.
+  if (isset($entity_info['submit callback']) && function_exists($entity_info['submit callback'])) {
+    $entity_info['submit callback']($entity, $form, $form_state);
+  }
+
+  // Allow fields to respond to the submission.
+  if ($entity_info['fieldable']) {
+    field_attach_submit($entity_type, $entity, $form, $form_state);
+  }
+
+  // Allow all modules to respond to the submission. Can't use entity_invoke(),
+  // because $form and $form_state must be passed. Pass $entity ahead of
+  // $entity_type for consistency with entity_invoke(), even though this is
+  // inconsistent with field_attach_submit().
+  foreach (module_implements('entity_submit') as $module) {
+    $function = $module . '_entity_submit';
+    $function($entity, $entity_type, $form, $form_state);
+  }
+
+  return $entity;
+}
+
+/**
  * Performs one or more XML-RPC request(s).
  *
  * @param $url
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.542
diff -u -p -r1.542 book.module
--- modules/book/book.module	6 May 2010 05:59:31 -0000	1.542
+++ modules/book/book.module	20 May 2010 21:49:53 -0000
@@ -403,7 +403,7 @@ function book_get_books() {
 function book_form_alter(&$form, &$form_state, $form_id) {
   if (!empty($form['#node_edit_form'])) {
     // Add elements to the node form.
-    $node = $form['#node'];
+    $node = $form_state['entity'];
 
     $access = user_access('administer book outlines');
     if (!$access) {
@@ -415,15 +415,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,
       );
     }
@@ -431,6 +429,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_state['entity']->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.875
diff -u -p -r1.875 comment.module
--- modules/comment/comment.module	10 May 2010 20:12:21 -0000	1.875
+++ modules/comment/comment.module	20 May 2010 21:49:53 -0000
@@ -98,6 +98,7 @@ function comment_entity_info() {
       'label' => t('Comment'),
       'base table' => 'comment',
       'uri callback' => 'comment_uri',
+      'submit callback' => 'comment_submit',
       'fieldable' => TRUE,
       'controller class' => 'CommentController',
       'entity keys' => array(
@@ -1138,7 +1139,7 @@ function comment_form_node_type_form_alt
  */
 function comment_form_alter(&$form, $form_state, $form_id) {
   if (!empty($form['#node_edit_form'])) {
-    $node = $form['#node'];
+    $node = $form_state['entity'];
     $form['comment_settings'] = array(
       '#type' => 'fieldset',
       '#access' => user_access('administer comments'),
@@ -1750,12 +1751,10 @@ function comment_form($form, &$form_stat
     $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(
+  $comment = entity_form_initialize($form, $form_state, 'comment', $comment);
+
+  // Ensure these default properties are present in the comment object.
+  $defaults = array(
     'name' => '',
     'mail' => '',
     'homepage' => '',
@@ -1766,7 +1765,11 @@ 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;
+    }
+  }
 
   // If not replying to a comment, use our dedicated page callback for new
   // comments on nodes.
@@ -1933,7 +1936,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;
@@ -1943,8 +1945,9 @@ 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);
+  $comment = entity_form_submit($form, $form_state);
   $form_state['comment_preview'] = comment_preview($comment);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -2010,7 +2013,11 @@ function comment_preview($comment) {
  */
 function comment_form_validate($form, &$form_state) {
   global $user;
+
+  // See http://drupal.org/node/367006#comment-2601522 for why we only validate
+  // a pseudo comment object rather than $form_state['entity'].
   $comment = (object) $form_state['values'];
+
   field_attach_form_validate('comment', $comment, $form, $form_state);
 
   if (!empty($form_state['values']['cid'])) {
@@ -2061,50 +2068,33 @@ function comment_form_validate($form, &$
 }
 
 /**
- * Prepare a comment for submission.
- *
- * @param $comment
- *   An associative array containing the comment data.
+ * Entity submit callback.
  */
 function comment_submit($comment) {
-  $comment += array('subject' => '');
-  if (empty($comment['date'])) {
-    $comment['date'] = 'now';
+  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;
 }
 
@@ -2113,7 +2103,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 = entity_form_submit($form, $form_state);
   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) {
@@ -2150,7 +2140,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.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.48
diff -u -p -r1.48 field.form.inc
--- modules/field/field.form.inc	30 Apr 2010 08:07:54 -0000	1.48
+++ modules/field/field.form.inc	20 May 2010 21:49:53 -0000
@@ -368,17 +368,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.8
diff -u -p -r1.8 field_test.entity.inc
--- modules/field/tests/field_test.entity.inc	6 May 2010 05:59:31 -0000	1.8
+++ modules/field/tests/field_test.entity.inc	20 May 2010 21:49:54 -0000
@@ -23,6 +23,7 @@ function field_test_entity_info() {
   return array(
     'test_entity' => array(
       'name' => t('Test Entity'),
+      'submit callback' => 'field_test_entity_test_entity_submit',
       'fieldable' => TRUE,
       'field cache' => FALSE,
       'entity keys' => array(
@@ -254,10 +255,7 @@ 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 = entity_form_initialize($form, $form_state);
 
   foreach (array('ftid', 'ftvid', 'fttype') as $key) {
     $form[$key] = array(
@@ -267,7 +265,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) {
@@ -300,7 +297,7 @@ function field_test_entity_form_validate
  * 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 = entity_form_submit($form, $form_state);
   $insert = empty($entity->ftid);
   field_test_entity_save($entity);
 
@@ -318,15 +315,11 @@ function field_test_entity_form_submit($
 }
 
 /**
- * Builds a test_entity from submitted form values.
+ * Entity submit callback.
  */
-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']);
+function field_test_entity_test_entity_submit($entity, $form, &$form_state) {
+  foreach (field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']) as $property => $value) {
+    $entity->$property = $value;
+  }
   $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/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.293
diff -u -p -r1.293 locale.module
--- modules/locale/locale.module	12 May 2010 08:26:14 -0000	1.293
+++ modules/locale/locale.module	20 May 2010 21:49:54 -0000
@@ -389,16 +389,16 @@ function locale_form_alter(&$form, &$for
     }
   }
   if (!empty($form['#node_edit_form'])) {
-    if (isset($form['#node']->type) && locale_multilingual_node_type($form['#node']->type)) {
+    if (isset($form_state['entity']->type) && locale_multilingual_node_type($form_state['entity']->type)) {
       $form['language'] = array(
         '#type' => 'select',
         '#title' => t('Language'),
-        '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''),
+        '#default_value' => (isset($form_state['entity']->language) ? $form_state['entity']->language : ''),
         '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
       );
     }
     // Node type without language selector: assign the default for new nodes
-    elseif (!isset($form['#node']->nid)) {
+    elseif (!isset($form_state['entity']->nid)) {
       $default = language_default();
       $form['language'] = array(
         '#type' => 'value',
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	20 May 2010 21:49:54 -0000
@@ -589,14 +589,13 @@ function menu_form_alter(&$form, $form_s
   if (!empty($form['#node_edit_form'])) {
     // Generate a list of possible parents.
     // @todo This must be handled in a #process handler.
-    $type = $form['#node']->type;
+    $type = $form_state['entity']->type;
     $options = menu_parent_options(menu_get_menus(), $type);
     // If no possible parent menu items were found, there is nothing to display.
     if (empty($options)) {
       return;
     }
-    $link = $form['#node']->menu;
-    $form['#submit'][] = 'menu_node_form_submit';
+    $link = $form_state['entity']->menu;
 
     $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_entity_submit().
  */
-function menu_node_form_submit($form, &$form_state) {
+function menu_entity_submit($entity, $entity_type, $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']);
+  if ($entity_type == 'node' && !empty($form_state['values']['menu']['parent'])) {
+    list($entity->menu['menu_name'], $entity->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.67
diff -u -p -r1.67 node.api.php
--- modules/node/node.api.php	5 May 2010 06:55:25 -0000	1.67
+++ modules/node/node.api.php	20 May 2010 21:49:55 -0000
@@ -666,13 +666,19 @@ function hook_node_update_index($node) {
  * the node at the validate stage, you can use form_set_value().
  *
  * @param $node
- *   The node being validated.
+ *   The node being validated. This is an object representation of
+ *   $form_state['values'] only. It does not contain node properties from
+ *   node_load() or node_object_prepare() that do not have corresponding entries
+ *   in $form_state['values']. $form_state['entity'] can be used to access the
+ *   node properties not being edited by the form.
  * @param $form
  *   The form being used to edit the node.
+ * @param $form_state
+ *   The form state array.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_validate($node, $form) {
+function hook_node_validate($node, $form, &$form_state) {
   if (isset($node->end) && isset($node->start)) {
     if ($node->start > $node->end) {
       form_set_error('time', t('An event may not end before it starts.'));
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1273
diff -u -p -r1.1273 node.module
--- modules/node/node.module	17 May 2010 07:43:36 -0000	1.1273
+++ modules/node/node.module	20 May 2010 21:49:55 -0000
@@ -176,6 +176,7 @@ function node_entity_info() {
       'base table' => 'node',
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
+      'submit callback' => 'node_submit',
       'fieldable' => TRUE,
       'entity keys' => array(
         'id' => 'nid',
@@ -929,11 +930,10 @@ function node_validate($node, $form = ar
 
   // Do node-type-specific validation checks.
   node_invoke($node, 'validate', $form);
-  module_invoke_all('node_validate', $node, $form);
 }
 
 /**
- * Prepare node for saving by populating author and creation date.
+ * Entity submit callback.
  */
 function node_submit($node) {
   global $user;
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	20 May 2010 21:49:55 -0000
@@ -75,13 +75,19 @@ function node_add($type) {
   return $output;
 }
 
+/**
+ * Validate the node add/edit form.
+ */
 function node_form_validate($form, &$form_state) {
+  // See http://drupal.org/node/367006#comment-2601522 for why we only validate
+  // a pseudo node object rather than $form_state['entity'].
   $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);
+  foreach (module_implements('node_validate') as $module) {
+    $function = $module . '_node_validate';
+    $function($node, $form, $form_state);
+  }
 }
 
 /**
@@ -89,14 +95,9 @@ 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);
-  }
+  $node = entity_form_initialize($form, $form_state, 'node', $node);
+
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -141,6 +142,9 @@ function node_form($form, &$form_state, 
     $form['title']['#weight'] = -5;
   }
 
+  // @todo Legacy support. Modules adding form building and processing functions
+  //   to the node form are encouraged to access the node using
+  //   $form_state['entity']. Remove in Drupal 8.
   $form['#node'] = $node;
 
   $form['additional_settings'] = array(
@@ -278,7 +282,6 @@ function node_form($form, &$form_state, 
   }
   $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;
@@ -293,14 +296,15 @@ function node_form_delete_submit($form, 
     $destination = drupal_get_destination();
     unset($_GET['destination']);
   }
-  $node = $form['#node'];
+  $node = $form_state['entity'];
   $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination));
 }
 
 
 function node_form_build_preview($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = entity_form_submit($form, $form_state);
   $form_state['node_preview'] = node_preview($node);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -382,7 +386,7 @@ function theme_node_preview($variables) 
 }
 
 function node_form_submit($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = entity_form_submit($form, $form_state);
   $insert = empty($node->nid);
   node_save($node);
   $node_link = l(t('view'), 'node/' . $node->nid);
@@ -398,7 +402,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,29 +410,13 @@ 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.
- */
-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);
-
-  $form_state['node'] = (array) $node;
-  $form_state['rebuild'] = TRUE;
-  return $node;
-}
-
-/**
  * Menu callback -- ask for confirmation of node deletion
  */
 function node_delete_confirm($form, &$form_state, $node) {
Index: modules/path/path.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/path/path.module,v
retrieving revision 1.183
diff -u -p -r1.183 path.module
--- modules/path/path.module	13 Feb 2010 21:41:58 -0000	1.183
+++ modules/path/path.module	20 May 2010 21:49:55 -0000
@@ -99,10 +99,10 @@ function path_menu() {
 function path_form_alter(&$form, $form_state, $form_id) {
   if (!empty($form['#node_edit_form'])) {
     $path = array();
-    if (!empty($form['#node']->nid)) {
-      $conditions = array('source' => 'node/' . $form['#node']->nid);
-      if ($form['#node']->language != LANGUAGE_NONE) {
-        $conditions['language'] = $form['#node']->language;
+    if (!empty($form_state['entity']->nid)) {
+      $conditions = array('source' => 'node/' . $form_state['entity']->nid);
+      if ($form_state['entity']->language != LANGUAGE_NONE) {
+        $conditions['language'] = $form_state['entity']->language;
       }
       $path = path_load($conditions);
       if ($path === FALSE) {
@@ -111,9 +111,9 @@ function path_form_alter(&$form, $form_s
     }
     $path += array(
       'pid' => NULL,
-      'source' => isset($form['#node']->nid) ? 'node/' . $form['#node']->nid : NULL,
+      'source' => isset($form_state['entity']->nid) ? 'node/' . $form_state['entity']->nid : NULL,
       'alias' => '',
-      'language' => isset($form['#node']->language) ? $form['#node']->language : LANGUAGE_NONE,
+      'language' => isset($form_state['entity']->language) ? $form_state['entity']->language : LANGUAGE_NONE,
     );
 
     $form['path'] = array(
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.348
diff -u -p -r1.348 poll.module
--- modules/poll/poll.module	9 May 2010 13:37:32 -0000	1.348
+++ modules/poll/poll.module	20 May 2010 21:49:55 -0000
@@ -352,15 +352,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_state['entity']->choice, which it does.
+  $form_state['entity']->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	20 May 2010 21:49: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/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.167
diff -u -p -r1.167 system.api.php
--- modules/system/system.api.php	7 May 2010 12:59:07 -0000	1.167
+++ modules/system/system.api.php	20 May 2010 21:49:56 -0000
@@ -90,6 +90,10 @@ function hook_hook_info_alter(&$hooks) {
  *   - uri callback: A function taking an entity as argument and returning the
  *     uri elements of the entity, e.g. 'path' and 'options'. The actual entity
  *     uri can be constructed by passing these elements to url().
+ *   - submit callback: A function that acts on an entity after a form
+ *     submission if the form submit handler requires an updated entity (e.g.,
+ *     in response to a "Save" or "Preview" button). See entity_form_submit().
+ *     This callback only updates the 
  *   - fieldable: Set to TRUE if you want your entity type to be fieldable.
  *   - entity keys: An array describing how the Field API can extract the
  *     information it needs from the objects of the type. Elements:
@@ -151,7 +155,8 @@ function hook_entity_info() {
       'controller class' => 'NodeController',
       'base table' => 'node',
       'revision table' => 'node_revision',
-      'path callback' => 'node_path',
+      'uri callback' => 'node_uri',
+      'submit callback' => 'node_submit',
       'fieldable' => TRUE,
       'entity keys' => array(
         'id' => 'nid',
@@ -242,6 +247,33 @@ function hook_entity_load($entities, $ty
 }
 
 /**
+ * Act on entities after a form submission, before the entity is saved, previewed, or used for building the next step in a multi-step form.
+ *
+ * Modules may use this hook to extract their data from $form_state and set it
+ * on $entity. Note that every key/value pair in $form_state['values'] is
+ * automatically added as a corresponding property/value pair in $entity, so
+ * modules only need to implement this hook to do something different than that.
+ *
+ * @param $entity
+ *   The entity being updated from data in $form_state.
+ * @param $type
+ *   The type of entity being updated (i.e. node, user, comment).
+ * @param $form
+ *   The form being used to edit the entity.
+ * @param $form_state
+ *   The form state array.
+ *
+ * @see entity_form_submit()
+ */
+function hook_entity_submit($entity, $type, $form, $form_state) {
+  // Decompose the selected menu parent option into 'menu_name' and 'plid', if
+  // the form used the default parent selection widget.
+  if ($entity_type == 'node' && !empty($form_state['values']['menu']['parent'])) {
+    list($entity->menu['menu_name'], $entity->menu['plid']) = explode(':', $form_state['values']['menu']['parent']);
+  }
+}
+
+/**
  * Act on entities when inserted.
  *
  * Generic insert hook called for all entity types via entity_invoke().
@@ -1255,7 +1287,8 @@ function hook_page_alter(&$page) {
  * Perform alterations before a form is rendered.
  *
  * One popular use of this hook is to add form elements to the node form. When
- * altering a node form, the node object can be accessed at $form['#node'].
+ * altering a node form, the node object can be accessed at
+ * $form_state['entity'].
  *
  * Note that instead of hook_form_alter(), which is called for all forms, you
  * can also use hook_form_FORM_ID_alter() to alter a specific form. For each
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	20 May 2010 21:49:56 -0000
@@ -630,20 +630,16 @@ function taxonomy_form_term($form, &$for
     'weight' => 0,
   );
 
-  // Take into account multi-step rebuilding.
-  if (isset($form_state['term'])) {
-    $edit = $form_state['term'] + $edit;
-  }
+  $term = entity_form_initialize($form, $form_state, 'taxonomy_term', (object) $edit);
 
-  $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 +648,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 +656,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 +682,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 +714,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 +724,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 +734,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,7 +755,11 @@ 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);
+  // See http://drupal.org/node/367006#comment-2601522 for why we only validate
+  // a pseudo term object rather than $form_state['entity'].
+  $term = (object) $form_state['values'];
+
+  field_attach_form_validate('taxonomy_term', $term, $form, $form_state);
 
   // Ensure numeric values.
   if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) {
@@ -790,7 +790,7 @@ function taxonomy_form_term_submit($form
     return;
   }
 
-  $term = taxonomy_form_term_submit_builder($form, $form_state);
+  $term = entity_form_submit($form, $form_state);
 
   $status = taxonomy_term_save($term);
   switch ($status) {
@@ -828,27 +828,6 @@ 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.
- */
-function taxonomy_form_term_submit_builder($form, &$form_state) {
-  $term = (object) $form_state['values'];
-
-  // 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;
 }
 
 /**
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.590
diff -u -p -r1.590 taxonomy.module
--- modules/taxonomy/taxonomy.module	14 May 2010 04:41:54 -0000	1.590
+++ modules/taxonomy/taxonomy.module	20 May 2010 21:49:56 -0000
@@ -88,6 +88,7 @@ function taxonomy_entity_info() {
       'controller class' => 'TaxonomyTermController',
       'base table' => 'taxonomy_term_data',
       'uri callback' => 'taxonomy_term_uri',
+      'submit callback' => 'taxonomy_term_submit',
       'fieldable' => TRUE,
       'entity keys' => array(
         'id' => 'tid',
@@ -453,6 +454,16 @@ function taxonomy_check_vocabulary_hiera
 }
 
 /**
+ * Entity submit callback.
+ */
+function taxonomy_term_submit($term, $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'];
+}
+
+/**
  * Save a term object to the database.
  *
  * @param $term
Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.79
diff -u -p -r1.79 translation.module
--- modules/translation/translation.module	16 Apr 2010 13:52:23 -0000	1.79
+++ modules/translation/translation.module	20 May 2010 21:49:57 -0000
@@ -124,8 +124,8 @@ function translation_form_node_type_form
  *   is about to be created.
  */
 function translation_form_alter(&$form, &$form_state, $form_id) {
-  if (!empty($form['#node_edit_form']) && translation_supported_type($form['#node']->type)) {
-    $node = $form['#node'];
+  if (!empty($form['#node_edit_form']) && translation_supported_type($form_state['entity']->type)) {
+    $node = $form_state['entity'];
     if (!empty($node->translation_source)) {
       // We are creating a translation. Add values and lock language field.
       $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source);
@@ -315,10 +315,12 @@ function translation_node_update($node) 
  *
  * Ensure that duplicate translations can not be created for the same source.
  */
-function translation_node_validate($node, $form) {
+function translation_node_validate($node, $form, $form_state) {
   // Only act on translatable nodes with a tnid or translation_source.
-  if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) {
-    $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid;
+  // Use $form_state['entity'] instead of $node for information about the node
+  // not being edited by this form.
+  if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form_state['entity']->translation_source->nid))) {
+    $tnid = !empty($node->tnid) ? $node->tnid : $form_state['entity']->translation_source->nid;
     $translations = translation_node_get_translations($tnid);
     if (isset($translations[$node->language]) && $translations[$node->language]->nid != $node->nid ) {
       form_set_error('language', t('There is already a translation in this language.'));
