diff --git includes/common.inc includes/common.inc
index 41561aa..074db2e 100644
--- includes/common.inc
+++ includes/common.inc
@@ -6626,6 +6626,50 @@ function entity_invoke($op, $entity_type, $entity) {
 }
 
 /**
+ * Helper function for attaching field API validation to entity forms.
+ */
+function entity_form_field_validate($entity_type, $form, &$form_state) {
+  // All field attach API functions act on an entity object, but during form
+  // validation, we don't have one. $form_state 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 created out of the form values.
+  $pseudo_entity = (object) $form_state['values'];
+  field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state);
+}
+
+/**
+ * Copies submitted values of non-field form elements to entity properties.
+ *
+ * During the submission handling of an entity form's "Save", "Preview", and
+ * possibly other buttons, the form state's entity needs to be updated with the
+ * submitted form values. Each entity form implements its own
+ * $form['#builder_function'] for doing this, appropriate for the particular
+ * entity and form. If the form is for a fieldable entity and has fields
+ * attached, that function should call field_attach_submit() for copying the
+ * field values to the entity. If the form has non-field form elements and needs
+ * those copied to entity properties, it may call this function, which copies
+ * $form_state['values'][PROPERTY] to $entity->PROPERTY for all entries in
+ * $form_state['values'] that are not field data. If the entity form includes
+ * elements with values that should not be copied to the entity in this way, it
+ * should not call this function and instead implement the required logic to get
+ * the correct form values into the entity.
+ */
+function entity_extract_non_field_form_values($entity_type, $entity, $form, &$form_state) {
+  $info = entity_get_info($entity_type);
+  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+
+  // Exclude fields, because they require special logic that is handled by
+  // field_attach_form_submit().
+  $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values'];
+
+  // Do not overwrite entity properties not being edited by the form.
+  foreach ($values_excluding_fields as $key => $value) {
+    $entity->$key = $value;
+  }
+}
+
+/**
  * Performs one or more XML-RPC request(s).
  *
  * @param $url
diff --git modules/book/book.module modules/book/book.module
index e6b9503..254048b 100644
--- modules/book/book.module
+++ modules/book/book.module
@@ -416,15 +416,13 @@ function book_form_alter(&$form, &$form_state, $form_id) {
 
     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_state, $form_id) {
 }
 
 /**
+ * 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
diff --git modules/comment/comment.module modules/comment/comment.module
index 376e684..5200430 100644
--- modules/comment/comment.module
+++ modules/comment/comment.module
@@ -1738,23 +1738,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(
+  // Add default properties to the comment object.
+  $defaults = array(
     'name' => '',
     'mail' => '',
     'homepage' => '',
@@ -1765,7 +1750,28 @@ function comment_form($form, &$form_state, $comment) {
     'language' => LANGUAGE_NONE,
     'uid' => 0,
   );
-  $comment = (object) $comment;
+  foreach ($defaults as $key => $value) {
+    if (!isset($comment->$key)) {
+      $comment->$key = $value;
+    }
+  }
+
+  // During initial form build, add the comment entity to the form state for
+  // use during form building and processing. During a rebuild, use what is in
+  // the form state.
+  $form_state += array('comment' => $comment);
+  $comment = $form_state['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.
@@ -1942,8 +1948,9 @@ function comment_form($form, &$form_state, $comment) {
  * 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 = $form['#builder_function']($form, $form_state);
   $form_state['comment_preview'] = comment_preview($comment);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -2009,8 +2016,8 @@ 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);
+
+  entity_form_field_validate('comment', $form, $form_state);
 
   if (!empty($form_state['values']['cid'])) {
     if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
@@ -2061,49 +2068,57 @@ function comment_form_validate($form, &$form_state) {
 
 /**
  * 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;
+  return $comment;
 }
 
 /**
- * Build a comment by processing form values and prepare for a form rebuild.
+ * Updates the form state's comment entity by processing this submission's values.
+ *
+ * This is the default #builder_function for the comment form. It is called
+ * during the "Save" and "Preview" submit handlers to retrieve the entity to
+ * save or preview. This function can also be called by a "Next" button of a
+ * wizard to update the form state's entity with the current step's values
+ * before proceeding to the next step.
+ *
+ * @see comment_form()
  */
 function comment_form_submit_build_comment($form, &$form_state) {
-  $comment = comment_submit($form_state['values']);
-
+  $comment = $form_state['comment'];
+  entity_extract_non_field_form_values('comment', $comment, $form, $form_state);
   field_attach_submit('comment', $comment, $form, $form_state);
-
-  $form_state['comment'] = (array) $comment;
-  $form_state['rebuild'] = TRUE;
+  comment_submit($comment);
   return $comment;
 }
 
@@ -2112,7 +2127,7 @@ function comment_form_submit_build_comment($form, &$form_state) {
  */
 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['#builder_function']($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) {
@@ -2149,7 +2164,6 @@ function comment_form_submit($form, &$form_state) {
     // 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.
diff --git modules/field/field.attach.inc modules/field/field.attach.inc
index 86401cd..784888f 100644
--- modules/field/field.attach.inc
+++ modules/field/field.attach.inc
@@ -556,9 +556,6 @@ function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcod
   $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) {
diff --git modules/field/field.default.inc modules/field/field.default.inc
index da80469..891c4de 100644
--- modules/field/field.default.inc
+++ modules/field/field.default.inc
@@ -65,25 +65,12 @@ function field_default_validate($entity_type, $entity, $field, $instance, $langc
 }
 
 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);
 }
 
 /**
diff --git modules/field/field.form.inc modules/field/field.form.inc
index 77bd861..4f14f67 100644
--- modules/field/field.form.inc
+++ modules/field/field.form.inc
@@ -359,17 +359,12 @@ function field_default_form_errors($entity_type, $entity, $field, $instance, $la
  * 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;
 }
 
 /**
diff --git modules/field/tests/field_test.entity.inc modules/field/tests/field_test.entity.inc
index 33461e3..f462330 100644
--- modules/field/tests/field_test.entity.inc
+++ modules/field/tests/field_test.entity.inc
@@ -264,10 +264,11 @@ 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;
+  // During initial form build, add the entity to the form state for use during
+  // form building and processing. During a rebuild, use what is in the form
+  // state.
+  $form_state += array('test_entity' => $entity);
+  $entity = $form_state['test_entity'];
 
   foreach (array('ftid', 'ftvid', 'fttype') as $key) {
     $form[$key] = array(
@@ -310,7 +311,7 @@ function field_test_entity_form_validate($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['#builder_function']($form, $form_state);
   $insert = empty($entity->ftid);
   field_test_entity_save($entity);
 
@@ -318,25 +319,21 @@ function field_test_entity_form_submit($form, &$form_state) {
   drupal_set_message($message);
 
   if ($entity->ftid) {
-    unset($form_state['rebuild']);
     $form_state['redirect'] = 'test-entity/' . $entity->ftid . '/edit';
   }
   else {
     // Error on save.
     drupal_set_message(t('The entity could not be saved.'), 'error');
+    $form_state['rebuild'] = TRUE;
   }
 }
 
 /**
- * Builds a test_entity from submitted form values.
+ * Updates the form state's entity by processing this submission's 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']);
+  $entity = $form_state['test_entity'];
+  entity_extract_non_field_form_values('test_entity', $entity, $form, $form_state);
   field_attach_submit('test_entity', $entity, $form, $form_state);
-
-  $form_state['test_entity'] = (array) $entity;
-  $form_state['rebuild'] = TRUE;
-
   return $entity;
 }
diff --git modules/menu/menu.module modules/menu/menu.module
index d72fbdd..25af93d 100644
--- modules/menu/menu.module
+++ modules/menu/menu.module
@@ -596,7 +596,6 @@ function menu_form_alter(&$form, $form_state, $form_id) {
       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_state, $form_id) {
 }
 
 /**
- * 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']);
   }
 }
 
diff --git modules/node/node.api.php modules/node/node.api.php
index 09a8b90..f37c35c 100644
--- modules/node/node.api.php
+++ modules/node/node.api.php
@@ -665,6 +665,34 @@ function hook_node_validate($node, $form) {
 }
 
 /**
+ * Act on a node after validated form values have been copied to it.
+ *
+ * This hook is invoked when a node form is submitted with either the "Save" or
+ * "Preview" button, after form values have been copied to the form state's node
+ * object, but before the node is saved or previewed. It is a chance for modules
+ * to adjust the node's properties from what they are simply after a copy from
+ * $form_state['values']. This hook is intended for adjusting non-field-related
+ * properties. See hook_field_attach_submit() for customizing field-related
+ * properties.
+ *
+ * @param $node
+ *   The node being updated in response to a form submission.
+ * @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
diff --git modules/node/node.pages.inc modules/node/node.pages.inc
index c39278c..a606306 100644
--- modules/node/node.pages.inc
+++ modules/node/node.pages.inc
@@ -76,12 +76,15 @@ function node_add($type) {
 }
 
 function node_form_validate($form, &$form_state) {
+  // $form_state['node'] contains the actual entity being edited, but we must
+  // not update it with form values that have not yet been validated, so we
+  // create a pseudo-entity to use 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);
+  entity_form_field_validate('node', $form, $form_state);
 }
 
 /**
@@ -89,14 +92,13 @@ function node_form_validate($form, &$form_state) {
  */
 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);
-  }
+  // During initial form build, add the node entity to the form state for use
+  // during form building and processing. During a rebuild, use what is in the
+  // form state.
+  $form_state += array('node' => $node);
+  $node = $form_state['node'];
+
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -140,7 +142,9 @@ function node_form($form, &$form_state, $node) {
   if (!isset($form['title']['#weight'])) {
     $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(
@@ -299,8 +303,9 @@ function node_form_delete_submit($form, &$form_state) {
 
 
 function node_form_build_preview($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = $form['#builder_function']($form, $form_state);
   $form_state['node_preview'] = node_preview($node);
+  $form_state['rebuild'] = TRUE;
 }
 
 /**
@@ -382,7 +387,7 @@ function theme_node_preview($variables) {
 }
 
 function node_form_submit($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = $form['#builder_function']($form, $form_state);
   $insert = empty($node->nid);
   node_save($node);
   $node_link = l(t('view'), 'node/' . $node->nid);
@@ -398,7 +403,6 @@ function node_form_submit($form, &$form_state) {
     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,25 +411,43 @@ function node_form_submit($form, &$form_state) {
     // 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 the form state's node entity by processing this submission's values.
+ *
+ * This is the default #builder_function for the node form. It is called
+ * during the "Save" and "Preview" submit handlers to retrieve the entity to
+ * save or preview. This function can also be called by a "Next" button of a
+ * wizard to update the form state's entity with the current step's values
+ * before proceeding to the next step.
+ *
+ * @see node_form()
  */
 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.
+  // @todo Legacy support for modules that extend the node form with form-level
+  //   submit handlers that adjust $form_state['values'] prior to those values
+  //   being used to update the entity. Module authors are encouraged to instead
+  //   adjust the node directly within a hook_node_submit() implementation. For
+  //   Drupal 8, evaluate whether the pattern of triggering form-level submit
+  //   handlers during button-level submit processing is worth supporting
+  //   properly, and if so, add a Form API function for doing so.
   unset($form_state['submit_handlers']);
   form_execute_handlers('submit', $form, $form_state);
-  $node = node_submit((object) $form_state['values']);
 
+  $node = $form_state['node'];
+  entity_extract_non_field_form_values('node', $node, $form, $form_state);
   field_attach_submit('node', $node, $form, $form_state);
 
-  $form_state['node'] = (array) $node;
-  $form_state['rebuild'] = TRUE;
+  node_submit($node);
+  foreach (module_implements('node_submit') as $module) {
+    $function = $module . '_node_submit';
+    $function($node, $form, $form_state);
+  }
   return $node;
 }
 
diff --git modules/poll/poll.module modules/poll/poll.module
index fd8d89a..a1d74b4 100644
--- modules/poll/poll.module
+++ modules/poll/poll.module
@@ -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_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) {
diff --git modules/simpletest/tests/form_test.module modules/simpletest/tests/form_test.module
index 45e5cd9..4db1de3 100644
--- modules/simpletest/tests/form_test.module
+++ modules/simpletest/tests/form_test.module
@@ -1143,8 +1143,6 @@ function form_test_form_user_register_form_alter(&$form, &$form_state) {
   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';
   }
 }
 
diff --git modules/taxonomy/taxonomy.admin.inc modules/taxonomy/taxonomy.admin.inc
index bba7f85..542e144 100644
--- modules/taxonomy/taxonomy.admin.inc
+++ modules/taxonomy/taxonomy.admin.inc
@@ -100,17 +100,28 @@ function theme_taxonomy_overview_vocabularies($variables) {
  * @see taxonomy_form_vocabulary_submit()
  */
 function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
+  // During initial form build, add the entity to the form state for use
+  // during form building and processing. During a rebuild, use what is in the
+  // form state.
   if (!is_array($edit)) {
-    $edit = (array) $edit;
-  }
-  $edit += array(
-    'name' => '',
-    'machine_name' => '',
-    'description' => '',
-    'hierarchy' => 0,
-    'weight' => 0,
-  );
-  $form['#vocabulary'] = (object) $edit;
+    // A vocabulary entity has been passed.
+    $form_state += array('vocabulary' => $edit);
+  }
+  elseif (empty($form_state['vocabulary'])) {
+    $edit += array(
+      'name' => '',
+      'machine_name' => '',
+      'description' => '',
+      'hierarchy' => 0,
+      'weight' => 0,
+    );
+    $form_state['vocabulary'] = (object) $edit;
+  }
+  $vocabulary = $form_state['vocabulary'];
+  // @todo Legacy support. Modules are encouraged to access the entity using
+  //   $form_state. Remove in Drupal 8.
+  $form['#vocabulary'] = $form_state['vocabulary'];
+
   // Check whether we need a deletion confirmation form.
   if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {
     return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['values']['vid']);
@@ -118,7 +129,7 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
   $form['name'] = array(
     '#type' => 'textfield',
     '#title' => t('Name'),
-    '#default_value' => $edit['name'],
+    '#default_value' => $vocabulary->name,
     '#maxlength' => 255,
     '#required' => TRUE,
     '#field_suffix' => ' <small id="edit-name-suffix">&nbsp;</small>',
@@ -139,7 +150,7 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
   $form['machine_name'] = array(
     '#type' => 'textfield',
     '#title' => t('Machine-readable name'),
-    '#default_value' => $edit['machine_name'],
+    '#default_value' => $vocabulary->machine_name,
     '#maxlength' => 255,
     '#description' => t('The unique machine-readable name for this vocabulary, used for theme templates. Can only contain lowercase letters, numbers, and underscores.'),
     '#required' => TRUE,
@@ -150,7 +161,7 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
   $form['description'] = array(
     '#type' => 'textfield',
     '#title' => t('Description'),
-    '#default_value' => $edit['description'],
+    '#default_value' => $vocabulary->description,
   );
   // Set the hierarchy to "multiple parents" by default. This simplifies the
   // vocabulary form and standardizes the term form.
@@ -161,10 +172,10 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
 
   $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
-  if (isset($edit['vid'])) {
+  if (isset($vocabulary->vid)) {
     $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
-    $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']);
-    $form['module'] = array('#type' => 'value', '#value' => $edit['module']);
+    $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid);
+    $form['module'] = array('#type' => 'value', '#value' => $vocabulary->module);
   }
   return $form;
 }
@@ -201,16 +212,17 @@ function taxonomy_form_vocabulary_validate($form, &$form_state) {
  * Accept the form submission for a vocabulary and save the results.
  */
 function taxonomy_form_vocabulary_submit($form, &$form_state) {
-  $old_vocabulary = $form['#vocabulary'];
   if ($form_state['clicked_button']['#value'] == t('Delete')) {
     // Rebuild the form to confirm vocabulary deletion.
     $form_state['rebuild'] = TRUE;
     $form_state['confirm_delete'] = TRUE;
     return;
   }
-  $vocabulary = (object) $form_state['values'];
-  if ($vocabulary->machine_name != $old_vocabulary->machine_name) {
-    field_attach_rename_bundle('taxonomy_term', $old_vocabulary->machine_name, $vocabulary->machine_name);
+  $old_machine_name = $form_state['vocabulary']->machine_name;
+  $vocabulary = $form_state['vocabulary'];
+  entity_extract_non_field_form_values('taxonomy_vocabulary', $vocabulary, $form, $form_state);
+  if ($vocabulary->machine_name != $old_machine_name) {
+    field_attach_rename_bundle('taxonomy_term', $old_machine_name, $vocabulary->machine_name);
   }
   switch (taxonomy_vocabulary_save($vocabulary)) {
     case SAVED_NEW:
@@ -617,33 +629,37 @@ function theme_taxonomy_overview_terms($variables) {
  * @see taxonomy_form_term_submit()
  */
 function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary = NULL) {
+  // During initial form build, add the term entity to the form state for use
+  // during form building and processing. During a rebuild, use what is in the
+  // form state.
   if (!isset($vocabulary) && is_object($edit)) {
+    // A term entity has been passed.
     $vocabulary = taxonomy_vocabulary_load($edit->vid);
-    $edit = (array) $edit;
-  }
-  $edit += array(
-    'name' => '',
-    'description' => '',
-    'format' => filter_default_format(),
-    'vocabulary_machine_name' => $vocabulary->machine_name,
-    'tid' => NULL,
-    'weight' => 0,
-  );
-
-  // Take into account multi-step rebuilding.
-  if (isset($form_state['term'])) {
-    $edit = $form_state['term'] + $edit;
+    $form_state += array('term' => $edit);
+  }
+  elseif (is_array($edit) && empty($form_state['term'])) {
+    // Initialize a new term entity with the passed data in $edit.
+    $edit += array(
+      'name' => '',
+      'description' => '',
+      'format' => filter_default_format(),
+      'vocabulary_machine_name' => $vocabulary->machine_name,
+      'tid' => NULL,
+      'weight' => 0,
+    );
+    $form_state['term'] = (object) $edit;
   }
+  $term = $form_state['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 +668,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
   $form['name'] = array(
     '#type' => 'textfield',
     '#title' => t('Name'),
-    '#default_value' => $edit['name'],
+    '#default_value' => $term->name,
     '#maxlength' => 255,
     '#required' => TRUE,
     '#weight' => -5,
@@ -660,18 +676,18 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
   $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 +702,23 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
   // 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 +734,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
     '#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 +744,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
   );
   $form['tid'] = array(
     '#type' => 'value',
-    '#value' => $edit['tid'],
+    '#value' => $term->tid,
   );
 
   $form['actions'] = array('#type' => 'actions');
@@ -738,7 +754,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
     '#weight' => 5,
   );
 
-  if ($edit['tid']) {
+  if ($term->tid) {
     $form['actions']['delete'] = array(
       '#type' => 'submit',
       '#value' => t('Delete'),
@@ -759,7 +775,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
  * @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);
+  entity_form_field_validate('taxonomy_term', $form, $form_state);
 
   // Ensure numeric values.
   if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) {
@@ -790,7 +806,7 @@ function taxonomy_form_term_submit($form, &$form_state) {
     return;
   }
 
-  $term = taxonomy_form_term_submit_builder($form, $form_state);
+  $term = $form['#builder_function']($form, $form_state);
 
   $status = taxonomy_term_save($term);
   switch ($status) {
@@ -833,21 +849,17 @@ function taxonomy_form_term_submit($form, &$form_state) {
 }
 
 /**
- * Build a term by processing form values and prepare for a form rebuild.
+ * Updates the form state's term entity by processing this submission's values.
  */
 function taxonomy_form_term_submit_builder($form, &$form_state) {
-  $term = (object) $form_state['values'];
+  $term = $form_state['term'];
+  entity_extract_non_field_form_values('taxonomy_term', $term, $form, $form_state);
+  field_attach_submit('taxonomy_term', $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'];
-
-  field_attach_submit('taxonomy_term', $term, $form, $form_state);
-
-  $form_state['term'] = (array) $term;
-  $form_state['rebuild'] = TRUE;
-
   return $term;
 }
 
diff --git modules/user/user.pages.inc modules/user/user.pages.inc
index 9bbf490..7f43b97 100644
--- modules/user/user.pages.inc
+++ modules/user/user.pages.inc
@@ -243,6 +243,14 @@ function template_preprocess_user_profile_category(&$variables) {
 function user_profile_form($form, &$form_state, $account, $category = 'account') {
   global $user;
 
+  // During initial form build, add the entity to the form state for use during
+  // form building and processing. During a rebuild, use what is in the form
+  // state.
+  $form_state += array('user' => $account);
+  $account = $form_state['user'];
+
+  // @todo Legacy support. Modules are encouraged to access the entity using
+  //   $form_state. Remove in Drupal 8.
   $form['#user'] = $account;
   $form['#user_category'] = $category;
 
@@ -278,22 +286,24 @@ function user_profile_form($form, &$form_state, $account, $category = 'account')
  * Validation function for the user account and profile editing form.
  */
 function user_profile_form_validate($form, &$form_state) {
-  $edit = (object) $form_state['values'];
-  field_attach_form_validate('user', $edit, $form, $form_state);
+  entity_form_field_validate('user', $form, $form_state);
 }
 
 /**
  * Submit function for the user account and profile editing form.
  */
 function user_profile_form_submit($form, &$form_state) {
-  $account = $form['#user'];
+  $account = $form_state['user'];
   $category = $form['#user_category'];
   // Remove unneeded values.
   form_state_values_clean($form_state);
 
-  $edit = (object) $form_state['values'];
-  field_attach_submit('user', $edit, $form, $form_state);
-  $edit = (array) $edit;
+  entity_extract_non_field_form_values('user', $account, $form, $form_state);
+  field_attach_submit('user', $account, $form, $form_state);
+
+  // Populate $edit with the properties of $account, which have been edited on
+  // this form by taking over all values, which appear in the form values too.
+  $edit = array_intersect_key((array) $account, $form_state['values']);
 
   user_save($account, $edit, $category);
   $form_state['values']['uid'] = $account->uid;
