? .cache
? .settings
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1052
diff -u -p -r1.1052 common.inc
--- includes/common.inc	26 Nov 2009 05:54:48 -0000	1.1052
+++ includes/common.inc	1 Dec 2009 22:24:22 -0000
@@ -4476,6 +4476,7 @@ function _drupal_bootstrap_full() {
   require_once DRUPAL_ROOT . '/includes/unicode.inc';
   require_once DRUPAL_ROOT . '/includes/image.inc';
   require_once DRUPAL_ROOT . '/includes/form.inc';
+  require_once DRUPAL_ROOT . '/includes/entity.inc';
   require_once DRUPAL_ROOT . '/includes/mail.inc';
   require_once DRUPAL_ROOT . '/includes/actions.inc';
   require_once DRUPAL_ROOT . '/includes/ajax.inc';
@@ -6330,11 +6331,15 @@ function entity_extract_ids($entity_type
  * @return
  *   An entity structure, initialized with the ids provided.
  */
-function entity_create_stub_entity($entity_type, $ids) {
-  $entity = new stdClass();
+function entity_create_stub_entity($entity_type, $ids = array()) {
   $info = entity_get_info($entity_type);
-  $entity->{$info['object keys']['id']} = $ids[0];
-  if (isset($info['object keys']['revision']) && !is_null($ids[1])) {
+  //TODO: property needs documentation
+  $class = isset($info['entity class']) ? $info['entity class'] : 'stdClass';
+  $entity = new $class();
+  if (isset($ids[0])) {
+    $entity->{$info['object keys']['id']} = $ids[0];
+  }
+  if (isset($info['object keys']['revision']) && isset($ids[1])) {
     $entity->{$info['object keys']['revision']} = $ids[1];
   }
   if ($info['object keys']['bundle']) {
Index: includes/entity.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/entity.inc,v
retrieving revision 1.3
diff -u -p -r1.3 entity.inc
--- includes/entity.inc	31 Oct 2009 16:06:35 -0000	1.3
+++ includes/entity.inc	1 Dec 2009 22:24:22 -0000
@@ -298,3 +298,176 @@ class DrupalDefaultEntityController impl
     $this->entityCache += $entities;
   }
 }
+
+/**
+ * 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.
+    if (!isset($object)) {
+      // @todo After creating a stub entity, we need to update $args, which is
+      //   currently not possible, because drupal_retrieve_form() uses private
+      //   $args instead of $form_state['build_info']['args'].
+      $object = entity_create_stub_entity($obj_type);
+    }
+    $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'];
+
+  // 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 = $form_state['storage']['object'];
+  
+  // Extract values.
+  foreach ($form_state['values'] as $key => $value) {
+    $object->$key = $value;
+  }
+
+  // Extraxt field values and validate attached fields.
+  field_attach_form_validate($obj_type, $object, $form, $form_state);
+
+  // Invoke entity validation function, if existent; i.e. ENTITY_validate().
+  $function = $obj_type . '_validate';
+  if (function_exists($function)) {
+    $function($object, $form, $form_state);
+  }
+  // Invoke hook_ENTITY_validate() implementations.
+  $hook = $obj_type . '_validate';
+  foreach (module_implements($hook) as $module) {
+    $function = $module . '_' . $hook;
+    $function($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) {
+  // 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);
+  }
+  // Invoke hook_ENTITY_submit() implementations.
+  $hook = $obj_type . '_submit';
+  foreach (module_implements($hook) as $module) {
+    $function = $module . '_' . $hook;
+    $function($object, $form, $form_state);
+  }
+
+  // Prepare submitted form values for attached fields for saving.
+  field_attach_submit($obj_type, $object, $form, $form_state);
+
+  // @todo Form submit handlers should set this on their own. Not even sure why
+  //   one can save a node with this being here (yet another FAPI bug?).
+  $form_state['rebuild'] = TRUE;
+}
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.411
diff -u -p -r1.411 form.inc
--- includes/form.inc	1 Dec 2009 16:28:57 -0000	1.411
+++ includes/form.inc	1 Dec 2009 22:24:22 -0000
@@ -484,6 +484,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.
@@ -511,10 +515,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'];
     }
@@ -541,7 +546,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/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.36
diff -u -p -r1.36 field.form.inc
--- modules/field/field.form.inc	1 Dec 2009 03:07:33 -0000	1.36
+++ modules/field/field.form.inc	1 Dec 2009 23:22:49 -0000
@@ -215,7 +215,7 @@ function field_multiple_value_form($fiel
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
           // Submit callback for disabled JavaScript.
-          '#submit' => array('field_add_more_submit'),
+          '#submit' => array('entity_form_submit', 'field_add_more_submit'),
           '#ajax' => array(
             'callback' => 'field_add_more_js',
             'wrapper' => $wrapper_id,
@@ -347,16 +347,15 @@ function field_default_form_errors($obj_
  */
 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);
+  $entity = $form_state['storage']['object'];
 
-    // 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]);
-    }
+  // 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($entity->{$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.1
diff -u -p -r1.1 field_test.entity.inc
--- modules/field/tests/field_test.entity.inc	20 Nov 2009 23:29:28 -0000	1.1
+++ modules/field/tests/field_test.entity.inc	1 Dec 2009 23:26:38 -0000
@@ -229,7 +229,7 @@ function field_test_entity_add($fttype) 
   $fttype = str_replace('-', '_', $fttype);
   $entity = (object)array('fttype' => $fttype);
   drupal_set_title(t('Create test_entity @bundle', array('@bundle' => $fttype)), PASS_THROUGH);
-  return drupal_get_form('field_test_entity_form', $entity, TRUE);
+  return entity_get_form('test_entity', 'field_test_entity_form', $entity, TRUE);
 }
 
 /**
@@ -237,18 +237,13 @@ function field_test_entity_add($fttype) 
  */
 function field_test_entity_edit($entity) {
   drupal_set_title(t('test_entity @ftid revision @ftvid', array('@ftid' => $entity->ftid, '@ftvid' => $entity->ftvid)), PASS_THROUGH);
-  return drupal_get_form('field_test_entity_form', $entity);
+  return entity_get_form('test_entity', 'field_test_entity_form', $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;
-
   foreach (array('ftid', 'ftvid', 'fttype') as $key) {
     $form[$key] = array(
       '#type' => 'value',
@@ -256,10 +251,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) {
     $form['revision'] = array(
       '#access' => user_access('administer field_test content'),
@@ -279,18 +270,11 @@ function field_test_entity_form($form, &
 }
 
 /**
- * Validate handler for field_test_entity_form().
- */
-function field_test_entity_form_validate($form, &$form_state) {
-  $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']);
-  field_attach_form_validate('test_entity', $entity, $form, $form_state);
-}
-
-/**
  * Submit handler for field_test_entity_form().
  */
 function field_test_entity_form_submit($form, &$form_state) {
-  $entity = field_test_entity_form_submit_builder($form, $form_state);
+  $entity = $form_state['storage']['object'];
+
   $insert = empty($entity->ftid);
   field_test_entity_save($entity);
 
@@ -298,7 +282,6 @@ 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 {
@@ -308,15 +291,8 @@ function field_test_entity_form_submit($
 }
 
 /**
- * Builds a test_entity from submitted form values.
+ * Entity submit callback.
  */
-function field_test_entity_form_submit_builder($form, &$form_state) {
-  $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']);
+function test_entity_submit($entity, $form, $form_state) {
   $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;
-}
+}
\ No newline at end of file
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1171
diff -u -p -r1.1171 node.module
--- modules/node/node.module	1 Dec 2009 13:14:42 -0000	1.1171
+++ modules/node/node.module	1 Dec 2009 22:24:22 -0000
@@ -926,8 +926,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.102
diff -u -p -r1.102 node.pages.inc
--- modules/node/node.pages.inc	1 Dec 2009 16:03:35 -0000	1.102
+++ modules/node/node.pages.inc	1 Dec 2009 22:24:55 -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'));
@@ -114,9 +108,7 @@ function node_form($form, &$form_state, 
     $form_state['input'] = array();
   }
 
-  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'];
   }
@@ -155,6 +147,7 @@ function node_form($form, &$form_state, 
     $form = array_merge_recursive($form, $extra);
   }
 
+  // @todo Remove.
   $form['#node'] = $node;
 
   $form['additional_settings'] = array(
@@ -276,14 +269,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(
@@ -293,11 +285,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;
 }
@@ -315,10 +305,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']);
 }
 
 /**
@@ -417,7 +405,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);
@@ -535,7 +523,7 @@ function node_revision_overview($node) {
     }
     $rows[] = array_merge($row, $operations);
   }
-  
+
   $build['node_revisions_table'] = array(
     '#theme' => 'table',
     '#rows' => $rows,
