Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.408
diff -u -p -r1.408 form.inc
--- includes/form.inc	28 Nov 2009 14:39:31 -0000	1.408
+++ includes/form.inc	29 Nov 2009 03:42:39 -0000
@@ -78,6 +78,170 @@ function drupal_get_form($form_id) {
 }
 
 /**
+ * Wrapper for drupal_build_form() to use for fieldable entity forms.
+ *
+ * This is similar to drupal_get_form(), but additionally
+ * - stores the entity type in $form_state['storage']['entity']; e.g. 'node' or
+ *   'user'.
+ * - stores the initial entity object in $form_state['storage']['object'] upon
+ *   first build of the form (but not on subsequent builds).
+ * - defines entity_form_wrapper() as 'wrapper_callback' to be invoked before
+ *   the form constructor is invoked, which is the heart of the special
+ *   fieldable entity form workflow.
+ *
+ * @see entity_form_wrapper()
+ *
+ * @param $obj_type
+ *   The type of $object; e.g. 'node' or 'user'.
+ * @param $form_id
+ *   The unique string identifying the desired form. See drupal_get_form() and
+ *   drupal_build_form() for details.
+ * @param $object
+ *   The object for which to build a form.
+ * @param ...
+ *   Any additional arguments to pass to form constructor functions.
+ *
+ * @see drupal_build_form()
+ */
+function entity_get_form($obj_type, $form_id, $object = NULL) {
+  // Handle additional arguments to pass to form constructor, i.e.
+  // remove $obj_type and $form_id, but pass on everything else.
+  $args = func_get_args();
+  array_shift($args);
+  array_shift($args);
+  $form_state['build_info']['args'] = $args;
+
+  // Prepare initial form storage.
+  if (empty($form_state['storage'])) {
+    // Store the name of the entity we are processing.
+    $form_state['storage']['entity'] = $obj_type;
+
+    // Prepare the initial object.
+    // The object in form storage is assigned by reference, so form constructor
+    // functions can work with either variable.
+    // @todo Can we build an empty object for entities? Or does this always
+    //   need to be prepared by a custom page callback?
+    //   @see node_add()
+    if (!isset($object)) {
+      $object = new stdClass;
+    }
+    $form_state['storage']['object'] = &$object;
+  }
+
+  // Invoke the common wrapper callback common for all fieldable entities.
+  $form_state['wrapper_callback'] = 'entity_form_wrapper';
+
+  return drupal_build_form($form_id, $form_state);
+}
+
+/**
+ * Form wrapper callback for fieldable entity forms.
+ *
+ * This function is invoked before the form constructor function (usually
+ * $form_id) to set up $form and $form_state for an fieldable entity form
+ * workflow:
+ * - form caching is enabled to cache all data in $form_state['storage']. It is
+ *   important that form caching must not be disabled by any function in the
+ *   entire form workflow. Otherwise, previously submitted form values will get
+ *   lost.
+ * - field widgets are attached to the form.
+ * - entity_form_validate() is added as form validation handler to validate
+ *   the submitted form values for fields and invoke an entity-specific
+ *   ENTITY_validate() function, if existent.
+ * - entity_form_submit() is added as form submit handler to update the object
+ *   in the form storage with the submitted form values, invoke an
+ *   entity-specific ENTITY_submit() function (if existent), and process field
+ *   values for attached fields.
+ *
+ * @see entity_get_form()
+ * @see entity_form_validate()
+ * @see entity_form_submit()
+ */
+function entity_form_wrapper($form, &$form_state) {
+  // Extract information that was initially setup by entity_get_form() or was
+  // reloaded from cache.
+  $obj_type = $form_state['storage']['entity'];
+  $object = $form_state['storage']['object'];
+
+  // Enable form caching to make form storage persist across requests. This is
+  // required; otherwise, entity_get_form() would re-populate variables in the
+  // form storage freshly based on the passed in arguments.
+  $form_state['cache'] = TRUE;
+
+  // Attach fields.
+  field_attach_form($obj_type, $object, $form, $form_state, (isset($object->language) ? $object->language : NULL));
+
+  // Add form validation handler for attached fields.
+  $form['#validate'][] = 'entity_form_validate';
+  // Add default form-level validation handler, i.e. CALLBACK_validate().
+  // Form buttons can override this by assigning custom validation handlers in
+  // their own #validate property.
+  // @todo drupal_prepare_form() actually adds FORMID_validate(), not callback.
+  if (function_exists($form_state['build_info']['callback'] . '_validate')) {
+    $form['#validate'][] = $form_state['build_info']['callback'] . '_validate';
+  }
+
+  // Add form submit handler for attached fields.
+  $form['#submit'][] = 'entity_form_submit';
+  // Add default form-level submit handler, i.e. CALLBACK_submit().
+  // Form buttons can override this by assigning custom submit handlers in
+  // their own #submit property.
+  // @todo drupal_prepare_form() actually adds FORMID_submit(), not callback.
+  if (function_exists($form_state['build_info']['callback'] . '_submit')) {
+    $form['#submit'][] = $form_state['build_info']['callback'] . '_submit';
+  }
+
+  return $form;
+}
+
+/**
+ * Form validation handler for fieldable entity forms.
+ *
+ * @see entity_form_wrapper()
+ * @see entity_form_submit()
+ */
+function entity_form_validate($form, &$form_state) {
+  // Extract information.
+  $obj_type = $form_state['storage']['entity'];
+  $object = (object) $form_state['values'];
+
+  // Invoke entity validation function, if existent; i.e. ENTITY_validate().
+  $function = $obj_type . '_validate';
+  if (function_exists($function)) {
+    $function($object, $form, $form_state);
+  }
+
+  // Validate attached fields.
+  field_attach_form_validate($obj_type, $object, $form, $form_state);
+}
+
+/**
+ * Form submit handler for fieldable entity forms.
+ *
+ * @see entity_form_wrapper()
+ * @see entity_form_validate()
+ */
+function entity_form_submit($form, &$form_state) {
+  // Update object in form storage with submitted values.
+  $form_state['storage']['object'] = (object) $form_state['values'];
+
+  // Extract information.
+  $obj_type = $form_state['storage']['entity'];
+  $object = &$form_state['storage']['object'];
+
+  // Invoke entity submit function, if existent; i.e. ENTITY_submit().
+  $function = $obj_type . '_submit';
+  if (function_exists($function)) {
+    $function($object, $form, $form_state);
+  }
+
+  // Prepare submitted form values for attached fields for saving.
+  field_attach_submit($obj_type, $object, $form, $form_state);
+
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
  * Build and process a form based on a form id.
  *
  * The form may also be retrieved from the cache if the form was built in a
@@ -485,6 +649,10 @@ function drupal_form_submit($form_id, &$
 function drupal_retrieve_form($form_id, &$form_state) {
   $forms = &drupal_static(__FUNCTION__);
 
+  // Initialize build information.
+  $form_state['build_info']['form_id'] = $form_id;
+  $form_state['build_info']['callback'] = $form_id;
+
   // We save two copies of the incoming arguments: one for modules to use
   // when mapping form ids to constructor functions, and another to pass to
   // the constructor function itself.
@@ -512,10 +680,11 @@ function drupal_retrieve_form($form_id, 
       $args = array_merge($form_definition['callback arguments'], $args);
     }
     if (isset($form_definition['callback'])) {
-      $callback = $form_definition['callback'];
+      $form_state['build_info']['callback'] = $form_definition['callback'];
     }
     // In case $form_state['wrapper_callback'] is not defined already, we also
     // allow hook_forms() to define one.
+    // @todo Technically, this belongs into build_info as well.
     if (!isset($form_state['wrapper_callback']) && isset($form_definition['wrapper_callback'])) {
       $form_state['wrapper_callback'] = $form_definition['wrapper_callback'];
     }
@@ -542,7 +711,8 @@ function drupal_retrieve_form($form_id, 
 
   // If $callback was returned by a hook_forms() implementation, call it.
   // Otherwise, call the function named after the form id.
-  $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
+  $form = call_user_func_array($form_state['build_info']['callback'], $args);
+  // @todo Remove; already in build_info now.
   $form['#form_id'] = $form_id;
 
   return $form;
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1169
diff -u -p -r1.1169 node.module
--- modules/node/node.module	19 Nov 2009 04:00:47 -0000	1.1169
+++ modules/node/node.module	29 Nov 2009 04:31:05 -0000
@@ -921,8 +921,6 @@ function node_submit($node) {
   }
   $node->created = !empty($node->date) ? strtotime($node->date) : REQUEST_TIME;
   $node->validated = TRUE;
-
-  return $node;
 }
 
 /**
Index: modules/node/node.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v
retrieving revision 1.100
diff -u -p -r1.100 node.pages.inc
--- modules/node/node.pages.inc	8 Nov 2009 10:02:41 -0000	1.100
+++ modules/node/node.pages.inc	29 Nov 2009 04:32:23 -0000
@@ -13,7 +13,7 @@
 function node_page_edit($node) {
   $type_name = node_type_get_name($node);
   drupal_set_title(t('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $node->title[FIELD_LANGUAGE_NONE][0]['value'])), PASS_THROUGH);
-  return drupal_get_form($node->type . '_node_form', $node);
+  return entity_get_form('node', $node->type . '_node_form', $node);
 }
 
 function node_add_page() {
@@ -65,21 +65,15 @@ function node_add($type) {
     $node = (object)array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $type, 'language' => '');
 
     drupal_set_title(t('Create @name', array('@name' => $types[$type]->name)), PASS_THROUGH);
-    $output = drupal_get_form($type . '_node_form', $node);
+    $output = entity_get_form('node', $node->type . '_node_form', $node);
   }
 
   return $output;
 }
 
-function node_form_validate($form, &$form_state) {
-  $node = (object)$form_state['values'];
-  node_validate($node, $form);
-
-  // Field validation. Requires access to $form_state, so this cannot be
-  // done in node_validate() as it currently exists.
-  field_attach_form_validate('node', $node, $form, $form_state);
-}
-
+/**
+ * @todo Why is this a separate function? Only invoked by node_form().
+ */
 function node_object_prepare($node) {
   // Set up default values, if required.
   $node_options = variable_get('node_options_' . $node->type, array('status', 'promote'));
@@ -110,9 +104,7 @@ function node_object_prepare($node) {
 function node_form($form, &$form_state, $node) {
   global $user;
 
-  if (isset($form_state['node'])) {
-    $node = (object)($form_state['node'] + (array)$node);
-  }
+  // @todo This is 100% #process.
   if (isset($form_state['node_preview'])) {
     $form['#prefix'] = $form_state['node_preview'];
   }
@@ -151,6 +143,7 @@ function node_form($form, &$form_state, 
     $form = array_merge_recursive($form, $extra);
   }
 
+  // @todo Remove.
   $form['#node'] = $node;
 
   $form['additional_settings'] = array(
@@ -272,14 +265,13 @@ function node_form($form, &$form_state, 
     '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])),
     '#value' => t('Save'),
     '#weight' => 5,
-    '#submit' => array('node_form_submit'),
   );
   $form['buttons']['preview'] = array(
     '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED,
     '#type' => 'submit',
     '#value' => t('Preview'),
     '#weight' => 10,
-    '#submit' => array('node_form_build_preview'),
+    '#submit' => array('entity_form_submit', 'node_form_build_preview'),
   );
   if (!empty($node->nid) && node_access('delete', $node)) {
     $form['buttons']['delete'] = array(
@@ -289,11 +281,9 @@ function node_form($form, &$form_state, 
       '#submit' => array('node_form_delete_submit'),
     );
   }
-  $form['#validate'][] = 'node_form_validate';
   $form['#theme'] = array($node->type . '_node_form', 'node_form');
 
   $form['#builder_function'] = 'node_form_submit_build_node';
-  field_attach_form('node', $node, $form, $form_state, $node->language);
 
   return $form;
 }
@@ -311,10 +301,8 @@ function node_form_delete_submit($form, 
   $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);
-  $form_state['node_preview'] = node_preview($node);
+  $form_state['node_preview'] = node_preview($form_state['storage']['object']);
 }
 
 /**
@@ -413,7 +401,7 @@ function theme_node_preview($variables) 
 }
 
 function node_form_submit($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
+  $node = $form_state['storage']['object'];
   $insert = empty($node->nid);
   node_save($node);
   $node_link = l(t('view'), 'node/' . $node->nid);
