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	23 May 2010 00:04:12 -0000
@@ -6624,6 +6624,97 @@ function entity_invoke($op, $entity_type
 }
 
 /**
+ * Initializes entity forms by associating the entity being edited.
+ *
+ * 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 implements a default association via standardized
+ * variables in $form and $form_state. Advanced entity forms (e.g., a form that
+ * needs to simultaneously edit multiple entities) may need to implement their
+ * own association instead of calling this function.
+ *
+ * @see entity_form_submit_update_entity()
+ */
+function entity_form_initialize(&$form, &$form_state, $entity_type, $entity) {
+  // The entity type is an unchangeable property of the form.
+  $form += array(
+    '#entity_type' => $entity_type,
+  );
+  // The entity will be updated by submit handlers and must persist across form
+  // rebuilds, so needs to be part of the form state.
+  $form_state += array(
+    $entity_type => $entity,
+  );
+  return $form_state[$entity_type];
+}
+
+/**
+ * Updates an entity form's associated entity object when requested by a form submit handler.
+ *
+ * An entity form submit handler may call this helper function when needing to
+ * update the entity associated with the form (e.g., in response to a "Preview"
+ * or "Save" button being clicked). This function implements generic processing
+ * that should be appropriate for many entity forms, but advanced entity forms
+ * may need to implement their own logic instead of calling this function.
+ *
+ * This function assumes a fully validated form, so do not call this function
+ * from submit handlers of buttons that use #limit_validation_errors. Such
+ * submit handlers must either update the entity themselves, using only
+ * validated data (e.g., poll_more_choices_submit()) or not update the entity at
+ * all if the form can be successfully rebuilt using the Form API's default
+ * mechanism of retaining user input during a rebuild (e.g.,
+ * field_add_more_submit()).
+ *
+ * @see entity_form_initialize()
+ */
+function entity_form_submit_update_entity($form, &$form_state, $entity_type = NULL, $entity = NULL) {
+  // Unless passed explicit entity parameters, retrieve them from their default
+  // form variables. See entity_form_initialize().
+  if (!isset($entity_type)) {
+    $entity_type = $form['#entity_type'];
+  }
+  if (!isset($entity)) {
+    $entity = $form_state[$entity_type];
+  }
+
+  // Map all form values to entity properties. The entity may contain properties
+  // not being edited by the form, and these must not be wiped.
+  foreach ($form_state['values'] as $key => $value) {
+    $entity->$key = $value;
+  }
+
+  // Allow entity forms to customize the mapping of non-field form values to the
+  // entity. For example, menu.module adds non-field form elements to the node
+  // form, and requires a function to customize how the submitted element values
+  // get mapped to $node. Modules needing to customize field-related entity data
+  // or entity data that depends on field_attach_submit() having run first may
+  // implement hook_field_attach_submit().
+  if (isset($form['#entity_submit'])) {
+    foreach ($form['#entity_submit'] as $function) {
+      $function($entity, $entity_type, $form, $form_state);
+    }
+  }
+
+  // Allow fields to customize how their form values map to the entity.
+  $entity_info = entity_get_info($entity_type);
+  if ($entity_info['fieldable']) {
+    field_attach_submit($entity_type, $entity, $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	23 May 2010 00:04:13 -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['node'];
 
     $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['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.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	23 May 2010 00:04:14 -0000
@@ -1138,7 +1138,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['node'];
     $form['comment_settings'] = array(
       '#type' => 'fieldset',
       '#access' => user_access('administer comments'),
@@ -1750,12 +1750,11 @@ 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);
+  $form['#entity_submit'][] = 'comment_submit';
+
+  // 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_update_entity($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['comment'].
   $comment = (object) $form_state['values'];
+
   field_attach_form_validate('comment', $comment, $form, $form_state);
 
   if (!empty($form_state['values']['cid'])) {
@@ -2063,48 +2070,40 @@ function comment_form_validate($form, &$
 /**
  * Prepare a comment for submission.
  *
- * @param $comment
- *   An associative array containing the comment data.
+ * @see comment_form()
+ * @see entity_form_submit_update_entity()
  */
 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;
   }
 
-  $comment['created'] = strtotime($comment['date']);
-  $comment['changed'] = REQUEST_TIME;
+  if (empty($comment->date)) {
+    $comment->date = 'now';
+  }
 
-  if (!empty($comment['name']) && ($account = user_load_by_name($comment['name']))) {
-    $comment['uid'] = $account->uid;
+  $comment->created = strtotime($comment->date);
+  $comment->changed = REQUEST_TIME;
+
+  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 +2112,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_update_entity($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 +2149,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	23 May 2010 00:04:14 -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	23 May 2010 00:04:15 -0000
@@ -254,10 +254,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, 'test_entity', $entity);
 
   foreach (array('ftid', 'ftvid', 'fttype') as $key) {
     $form[$key] = array(
@@ -267,7 +264,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 +296,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_update_entity($form, $form_state);
   $insert = empty($entity->ftid);
   field_test_entity_save($entity);
 
@@ -308,25 +304,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/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	23 May 2010 00:04:15 -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['node']->type) && locale_multilingual_node_type($form_state['node']->type)) {
       $form['language'] = array(
         '#type' => 'select',
         '#title' => t('Language'),
-        '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''),
+        '#default_value' => (isset($form_state['node']->language) ? $form_state['node']->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['node']->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	23 May 2010 00:04:15 -0000
@@ -589,14 +589,14 @@ 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['node']->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['node']->menu;
+    $form['#entity_submit'][] = 'menu_node_form_entity_submit';
 
     $form['menu'] = array(
       '#type' => 'fieldset',
@@ -661,15 +661,16 @@ function menu_form_alter(&$form, $form_s
 }
 
 /**
- * Submit handler for node form.
+ * Maps submitted values for the menu selection widget to the needed node properties.
  *
  * @see menu_form_alter()
+ * @see entity_form_submit_update_entity()
  */
-function menu_node_form_submit($form, &$form_state) {
+function menu_node_form_entity_submit($node, $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']);
+    list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']);
   }
 }
 
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	23 May 2010 00:04:16 -0000
@@ -934,6 +934,9 @@ function node_validate($node, $form = ar
 
 /**
  * Prepare node for saving by populating author and creation date.
+ *
+ * @see node_form()
+ * @see entity_form_submit_update_entity()
  */
 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	23 May 2010 00:04:16 -0000
@@ -76,6 +76,8 @@ function node_add($type) {
 }
 
 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['node'].
   $node = (object) $form_state['values'];
   node_validate($node, $form);
 
@@ -89,14 +91,10 @@ 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);
+  $form['#entity_submit'][] = 'node_submit';
+
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -141,6 +139,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['node']. Remove in Drupal 8.
   $form['#node'] = $node;
 
   $form['additional_settings'] = array(
@@ -278,7 +279,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 +293,15 @@ function node_form_delete_submit($form, 
     $destination = drupal_get_destination();
     unset($_GET['destination']);
   }
-  $node = $form['#node'];
+  $node = $form_state['node'];
   $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 = node_form_submit_update_node($form, $form_state);
   $form_state['node_preview'] = node_preview($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 = node_form_submit_update_node($form, $form_state);
   $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,30 @@ 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.
+ * Updates a node form's associated node object when requested by a submit handler.
  */
-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.
+function node_form_submit_update_node($form, &$form_state) {
+  // @todo Legacy support for modules that extend the node form with form-level
+  //   submit handlers that need to update $form_state prior to the node being
+  //   updated. For the node 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 a #entity_submit
+  //   function instead (see menu_form_alter() for an example). Remove this in
+  //   Drupal 8.
   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;
+  // Other than the above legacy support, we can re-use the generic entity
+  // updating function.
+  return entity_form_submit_update_entity($form, $form_state);
 }
 
 /**
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	23 May 2010 00:04:16 -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['node']->nid)) {
+      $conditions = array('source' => 'node/' . $form_state['node']->nid);
+      if ($form_state['node']->language != LANGUAGE_NONE) {
+        $conditions['language'] = $form_state['node']->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['node']->nid) ? 'node/' . $form_state['node']->nid : NULL,
       'alias' => '',
-      'language' => isset($form['#node']->language) ? $form['#node']->language : LANGUAGE_NONE,
+      'language' => isset($form_state['node']->language) ? $form_state['node']->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	23 May 2010 00:04:16 -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['node']->choice, which it does.
+  $form_state['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	23 May 2010 00:04:17 -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	23 May 2010 00:04:17 -0000
@@ -1255,7 +1255,7 @@ 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['node'].
  *
  * 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	23 May 2010 00:04:17 -0000
@@ -630,20 +630,17 @@ 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);
+  $form['#entity_submit'][] = 'taxonomy_term_submit';
 
-  $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 +649,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 +657,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 +683,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 +715,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 +725,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 +735,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 +756,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['taxonomy_term'].
+  $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 +791,7 @@ function taxonomy_form_term_submit($form
     return;
   }
 
-  $term = taxonomy_form_term_submit_builder($form, $form_state);
+  $term = entity_form_submit_update_entity($form, $form_state);
 
   $status = taxonomy_term_save($term);
   switch ($status) {
@@ -828,27 +829,19 @@ 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.
+ * Prepare a taxonomy term for submission.
+ *
+ * @see taxonomy_form_term()
+ * @see entity_form_submit_update_entity()
  */
-function taxonomy_form_term_submit_builder($form, &$form_state) {
-  $term = (object) $form_state['values'];
-
+function taxonomy_term_submit($term, $entity_type, $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;
 }
 
 /**
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	23 May 2010 00:04:17 -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['node']->type)) {
+    $node = $form_state['node'];
     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);
