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 21 May 2010 00:06:05 -0000 @@ -6624,6 +6624,90 @@ function entity_invoke($op, $entity_type } /** + * Called from the form builder of an entity form. + */ +function entity_form_initialize($form, &$form_state, $entity_type, $entity) { + if (!isset($form_state['entity'])) { + $form_state['entity_type'] = $entity_type; + $form_state['entity'] = $entity; + } + return $form_state['entity']; +} + +/** + * Update the entity being edited in response to a form submission. + * + * This function is called by submit handlers of buttons on an entity editing + * form that require $form_state['entity'] to be updated based on the submitted + * form values. For example, the submit handler of the "Save" button requires an + * updated entity to save to the database, and the submit handler of the + * "Preview" button requires an updated entity to preview. Other modules may add + * additional buttons with submit handlers that similarly require an updated + * entity. + * + * This function invokes callbacks that may assume a fully validated form, so + * do not call this function from button-level submit handlers of buttons that + * use #limit_validation_errors. Such submit handlers need to either update the + * entity themselves with data that has been validated (see + * poll_more_choices_submit()) or not update the entity at all if the form can + * be successfully rebuilt using the Form APIs default mechanism of retaining + * user input during a rebuild (see field_add_more_submit()). + * + * @param $form + * The form being submitted. + * @param $form_state + * The form state array. $form_state['entity'] contains the entity that needs + * to be updated. + */ +function entity_form_submit($form, &$form_state) { + // @todo Legacy support for modules that extend the node edit form with + // form-level submit handlers that need to update $form_state prior to the + // entity being updated. For the entity to be updated properly during a + // multi-step workflow, we must call these handlers even when this function + // is called from a button-level submit handler (e.g., + // node_form_build_preview()). Module authors are encouraged to convert to + // using hook_entity_submit(). Remove this in Drupal 8. Only the node edit + // form requires this backwards compatibility. + if (!empty($form['#node_edit_form'])) { + unset($form_state['submit_handlers']); + form_execute_handlers('submit', $form, $form_state); + } + + // Get information about the entity being submitted. + $entity_type = $form_state['entity_type']; + $entity = $form_state['entity']; + $entity_info = entity_get_info($entity_type); + + // The entity may contain properties not being edited by the form, and these + // must not be wiped. For anything that is being edited by the form, update + // the entity with the new values. + foreach ($form_state['values'] as $key => $value) { + $entity->$key = $value; + } + + // Invoke the entity submit callback. + if (isset($entity_info['submit callback']) && function_exists($entity_info['submit callback'])) { + $entity_info['submit callback']($entity, $form, $form_state); + } + + // Allow fields to respond to the submission. + if ($entity_info['fieldable']) { + field_attach_submit($entity_type, $entity, $form, $form_state); + } + + // Allow all modules to respond to the submission. Can't use entity_invoke(), + // because $form and $form_state must be passed. Pass $entity ahead of + // $entity_type for consistency with entity_invoke(), even though this is + // inconsistent with field_attach_submit(). + foreach (module_implements('entity_submit') as $module) { + $function = $module . '_entity_submit'; + $function($entity, $entity_type, $form, $form_state); + } + + return $entity; +} + +/** * Performs one or more XML-RPC request(s). * * @param $url Index: modules/book/book.module =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.module,v retrieving revision 1.542 diff -u -p -r1.542 book.module --- modules/book/book.module 6 May 2010 05:59:31 -0000 1.542 +++ modules/book/book.module 21 May 2010 00:06:07 -0000 @@ -403,7 +403,7 @@ function book_get_books() { function book_form_alter(&$form, &$form_state, $form_id) { if (!empty($form['#node_edit_form'])) { // Add elements to the node form. - $node = $form['#node']; + $node = $form_state['entity']; $access = user_access('administer book outlines'); if (!$access) { @@ -415,15 +415,13 @@ function book_form_alter(&$form, &$form_ if ($access) { _book_add_form_elements($form, $form_state, $node); + // Since the "Book" dropdown can't trigger a form submission when + // JavaScript is disabled, add a submit button to do that. book.css hides + // this button when JavaScript is enabled. $form['book']['pick-book'] = array( '#type' => 'submit', '#value' => t('Change book (update list of parents)'), - // Submit the node form so the parent select options get updated. - // This is typically only used when JS is disabled. Since the parent options - // won't be changed via AJAX, a button is provided in the node form to submit - // the form and generate options in the parent select corresponding to the - // selected book. This is similar to what happens during a node preview. - '#submit' => array('node_form_submit_build_node'), + '#submit' => array('book_pick_book_nojs_submit'), '#weight' => 20, ); } @@ -431,6 +429,22 @@ function book_form_alter(&$form, &$form_ } /** + * Submit handler to change a node's book. + * + * This handler is run when JavaScript is disabled. It triggers the form to + * rebuild so that the "Parent item" options are changed to reflect the newly + * selected book. When JavaScript is enabled, the submit button that triggers + * this handler is hidden, and the "Book" dropdown directly triggers the + * book_form_update() AJAX callback instead. + * + * @see book_form_update() + */ +function book_pick_book_nojs_submit($form, &$form_state) { + $form_state['entity']->book = $form_state['values']['book']; + $form_state['rebuild'] = TRUE; +} + +/** * Build the parent selection form element for the node form or outline tab. * * This function is also called when generating a new set of options during the Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.875 diff -u -p -r1.875 comment.module --- modules/comment/comment.module 10 May 2010 20:12:21 -0000 1.875 +++ modules/comment/comment.module 21 May 2010 00:06:07 -0000 @@ -98,6 +98,7 @@ function comment_entity_info() { 'label' => t('Comment'), 'base table' => 'comment', 'uri callback' => 'comment_uri', + 'submit callback' => 'comment_submit', 'fieldable' => TRUE, 'controller class' => 'CommentController', 'entity keys' => array( @@ -1138,7 +1139,7 @@ function comment_form_node_type_form_alt */ function comment_form_alter(&$form, $form_state, $form_id) { if (!empty($form['#node_edit_form'])) { - $node = $form['#node']; + $node = $form_state['entity']; $form['comment_settings'] = array( '#type' => 'fieldset', '#access' => user_access('administer comments'), @@ -1750,12 +1751,10 @@ function comment_form($form, &$form_stat $form['#attributes']['class'][] = 'user-info-from-cookie'; } - $comment = (array) $comment; - // Take into account multi-step rebuilding. - if (isset($form_state['comment'])) { - $comment = $form_state['comment'] + (array) $comment; - } - $comment += array( + $comment = entity_form_initialize($form, $form_state, 'comment', $comment); + + // Ensure these default properties are present in the comment object. + $defaults = array( 'name' => '', 'mail' => '', 'homepage' => '', @@ -1766,7 +1765,11 @@ function comment_form($form, &$form_stat 'language' => LANGUAGE_NONE, 'uid' => 0, ); - $comment = (object) $comment; + foreach ($defaults as $key => $value) { + if (!isset($comment->$key)) { + $comment->$key = $value; + } + } // If not replying to a comment, use our dedicated page callback for new // comments on nodes. @@ -1933,7 +1936,6 @@ function comment_form($form, &$form_stat // Attach fields. $comment->node_type = 'comment_node_' . $node->type; - $form['#builder_function'] = 'comment_form_submit_build_comment'; field_attach_form('comment', $comment, $form, $form_state); return $form; @@ -1943,8 +1945,9 @@ function comment_form($form, &$form_stat * Build a preview from submitted form values. */ function comment_form_build_preview($form, &$form_state) { - $comment = comment_form_submit_build_comment($form, $form_state); + $comment = entity_form_submit($form, $form_state); $form_state['comment_preview'] = comment_preview($comment); + $form_state['rebuild'] = TRUE; } /** @@ -2010,7 +2013,11 @@ function comment_preview($comment) { */ function comment_form_validate($form, &$form_state) { global $user; + + // See http://drupal.org/node/367006#comment-2601522 for why we only validate + // a pseudo comment object rather than $form_state['entity']. $comment = (object) $form_state['values']; + field_attach_form_validate('comment', $comment, $form, $form_state); if (!empty($form_state['values']['cid'])) { @@ -2061,50 +2068,33 @@ function comment_form_validate($form, &$ } /** - * Prepare a comment for submission. - * - * @param $comment - * An associative array containing the comment data. + * Entity submit callback. */ function comment_submit($comment) { - $comment += array('subject' => ''); - if (empty($comment['date'])) { - $comment['date'] = 'now'; + if (empty($comment->date)) { + $comment->date = 'now'; } - $comment['created'] = strtotime($comment['date']); - $comment['changed'] = REQUEST_TIME; + $comment->created = strtotime($comment->date); + $comment->changed = REQUEST_TIME; - if (!empty($comment['name']) && ($account = user_load_by_name($comment['name']))) { - $comment['uid'] = $account->uid; + if (!empty($comment->name) && ($account = user_load_by_name($comment->name))) { + $comment->uid = $account->uid; } // Validate the comment's subject. If not specified, extract from comment body. - if (trim($comment['subject']) == '') { + if (trim($comment->subject) == '') { // The body may be in any format, so: // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. - $comment['subject'] = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment['comment_body'][LANGUAGE_NONE][0]['value'], $comment['comment_body'][LANGUAGE_NONE][0]['format'])))), 29, TRUE); + $comment->subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment->comment_body[LANGUAGE_NONE][0]['value'], $comment->comment_body[LANGUAGE_NONE][0]['format'])))), 29, TRUE); // Edge cases where the comment body is populated only by HTML tags will // require a default subject. - if ($comment['subject'] == '') { - $comment['subject'] = t('(No subject)'); + if ($comment->subject == '') { + $comment->subject = t('(No subject)'); } } - return (object) $comment; -} - -/** - * Build a comment by processing form values and prepare for a form rebuild. - */ -function comment_form_submit_build_comment($form, &$form_state) { - $comment = comment_submit($form_state['values']); - - field_attach_submit('comment', $comment, $form, $form_state); - - $form_state['comment'] = (array) $comment; - $form_state['rebuild'] = TRUE; return $comment; } @@ -2113,7 +2103,7 @@ function comment_form_submit_build_comme */ function comment_form_submit($form, &$form_state) { $node = node_load($form_state['values']['nid']); - $comment = comment_form_submit_build_comment($form, $form_state); + $comment = entity_form_submit($form, $form_state); if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) { // Save the anonymous user information to a cookie for reuse. if (!$comment->uid) { @@ -2150,7 +2140,6 @@ function comment_form_submit($form, &$fo // Redirect the user to the node they are commenting on. $redirect = 'node/' . $node->nid; } - unset($form_state['rebuild']); $form_state['redirect'] = $redirect; // Clear the block and page caches so that anonymous users see the comment // they have posted. Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.48 diff -u -p -r1.48 field.form.inc --- modules/field/field.form.inc 30 Apr 2010 08:07:54 -0000 1.48 +++ modules/field/field.form.inc 21 May 2010 00:06:07 -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 21 May 2010 00:06:08 -0000 @@ -23,6 +23,7 @@ function field_test_entity_info() { return array( 'test_entity' => array( 'name' => t('Test Entity'), + 'submit callback' => 'field_test_entity_test_entity_submit', 'fieldable' => TRUE, 'field cache' => FALSE, 'entity keys' => array( @@ -254,10 +255,7 @@ function field_test_entity_edit($entity) * Test_entity form. */ function field_test_entity_form($form, &$form_state, $entity, $add = FALSE) { - if (isset($form_state['test_entity'])) { - $entity = $form_state['test_entity'] + (array) $entity; - } - $entity = (object) $entity; + $entity = entity_form_initialize($form, $form_state, 'test_entity', $entity); foreach (array('ftid', 'ftvid', 'fttype') as $key) { $form[$key] = array( @@ -267,7 +265,6 @@ function field_test_entity_form($form, & } // Add field widgets. - $form['#builder_function'] = 'field_test_entity_form_submit_builder'; field_attach_form('test_entity', $entity, $form, $form_state); if (!$add) { @@ -300,7 +297,7 @@ function field_test_entity_form_validate * Submit handler for field_test_entity_form(). */ function field_test_entity_form_submit($form, &$form_state) { - $entity = field_test_entity_form_submit_builder($form, $form_state); + $entity = entity_form_submit($form, $form_state); $insert = empty($entity->ftid); field_test_entity_save($entity); @@ -318,15 +315,11 @@ function field_test_entity_form_submit($ } /** - * Builds a test_entity from submitted form values. + * Entity submit callback. */ -function field_test_entity_form_submit_builder($form, &$form_state) { - $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']); +function field_test_entity_test_entity_submit($entity, $form, &$form_state) { + foreach (field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']) as $property => $value) { + $entity->$property = $value; + } $entity->revision = !empty($form_state['values']['revision']); - field_attach_submit('test_entity', $entity, $form, $form_state); - - $form_state['test_entity'] = (array) $entity; - $form_state['rebuild'] = TRUE; - - return $entity; } Index: modules/locale/locale.module =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v retrieving revision 1.293 diff -u -p -r1.293 locale.module --- modules/locale/locale.module 12 May 2010 08:26:14 -0000 1.293 +++ modules/locale/locale.module 21 May 2010 00:06:08 -0000 @@ -389,16 +389,16 @@ function locale_form_alter(&$form, &$for } } if (!empty($form['#node_edit_form'])) { - if (isset($form['#node']->type) && locale_multilingual_node_type($form['#node']->type)) { + if (isset($form_state['entity']->type) && locale_multilingual_node_type($form_state['entity']->type)) { $form['language'] = array( '#type' => 'select', '#title' => t('Language'), - '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''), + '#default_value' => (isset($form_state['entity']->language) ? $form_state['entity']->language : ''), '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), ); } // Node type without language selector: assign the default for new nodes - elseif (!isset($form['#node']->nid)) { + elseif (!isset($form_state['entity']->nid)) { $default = language_default(); $form['language'] = array( '#type' => 'value', Index: modules/menu/menu.module =================================================================== RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v retrieving revision 1.229 diff -u -p -r1.229 menu.module --- modules/menu/menu.module 7 Mar 2010 07:55:14 -0000 1.229 +++ modules/menu/menu.module 21 May 2010 00:06:08 -0000 @@ -589,14 +589,13 @@ function menu_form_alter(&$form, $form_s if (!empty($form['#node_edit_form'])) { // Generate a list of possible parents. // @todo This must be handled in a #process handler. - $type = $form['#node']->type; + $type = $form_state['entity']->type; $options = menu_parent_options(menu_get_menus(), $type); // If no possible parent menu items were found, there is nothing to display. if (empty($options)) { return; } - $link = $form['#node']->menu; - $form['#submit'][] = 'menu_node_form_submit'; + $link = $form_state['entity']->menu; $form['menu'] = array( '#type' => 'fieldset', @@ -661,15 +660,13 @@ function menu_form_alter(&$form, $form_s } /** - * Submit handler for node form. - * - * @see menu_form_alter() + * Implements hook_entity_submit(). */ -function menu_node_form_submit($form, &$form_state) { +function menu_entity_submit($entity, $entity_type, $form, $form_state) { // Decompose the selected menu parent option into 'menu_name' and 'plid', if // the form used the default parent selection widget. - if (!empty($form_state['values']['menu']['parent'])) { - list($form_state['values']['menu']['menu_name'], $form_state['values']['menu']['plid']) = explode(':', $form_state['values']['menu']['parent']); + if ($entity_type == 'node' && !empty($form_state['values']['menu']['parent'])) { + list($entity->menu['menu_name'], $entity->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); } } Index: modules/node/node.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v retrieving revision 1.67 diff -u -p -r1.67 node.api.php --- modules/node/node.api.php 5 May 2010 06:55:25 -0000 1.67 +++ modules/node/node.api.php 21 May 2010 00:06:09 -0000 @@ -666,13 +666,19 @@ function hook_node_update_index($node) { * the node at the validate stage, you can use form_set_value(). * * @param $node - * The node being validated. + * The node being validated. This is an object representation of + * $form_state['values'] only. It does not contain node properties from + * node_load() or node_object_prepare() that do not have corresponding entries + * in $form_state['values']. $form_state['entity'] can be used to access the + * node properties not being edited by the form. * @param $form * The form being used to edit the node. + * @param $form_state + * The form state array. * * @ingroup node_api_hooks */ -function hook_node_validate($node, $form) { +function hook_node_validate($node, $form, &$form_state) { if (isset($node->end) && isset($node->start)) { if ($node->start > $node->end) { form_set_error('time', t('An event may not end before it starts.')); Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1273 diff -u -p -r1.1273 node.module --- modules/node/node.module 17 May 2010 07:43:36 -0000 1.1273 +++ modules/node/node.module 21 May 2010 00:06:09 -0000 @@ -176,6 +176,7 @@ function node_entity_info() { 'base table' => 'node', 'revision table' => 'node_revision', 'uri callback' => 'node_uri', + 'submit callback' => 'node_submit', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'nid', @@ -929,11 +930,10 @@ function node_validate($node, $form = ar // Do node-type-specific validation checks. node_invoke($node, 'validate', $form); - module_invoke_all('node_validate', $node, $form); } /** - * Prepare node for saving by populating author and creation date. + * Entity submit callback. */ function node_submit($node) { global $user; Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.126 diff -u -p -r1.126 node.pages.inc --- modules/node/node.pages.inc 10 May 2010 06:34:39 -0000 1.126 +++ modules/node/node.pages.inc 21 May 2010 00:06:09 -0000 @@ -75,13 +75,19 @@ function node_add($type) { return $output; } +/** + * Validate the node add/edit form. + */ function node_form_validate($form, &$form_state) { + // See http://drupal.org/node/367006#comment-2601522 for why we only validate + // a pseudo node object rather than $form_state['entity']. $node = (object) $form_state['values']; node_validate($node, $form); - - // Field validation. Requires access to $form_state, so this cannot be - // done in node_validate() as it currently exists. field_attach_form_validate('node', $node, $form, $form_state); + foreach (module_implements('node_validate') as $module) { + $function = $module . '_node_validate'; + $function($node, $form, $form_state); + } } /** @@ -89,14 +95,9 @@ function node_form_validate($form, &$for */ function node_form($form, &$form_state, $node) { global $user; - // This form has its own multistep persistence. - if ($form_state['rebuild']) { - $form_state['input'] = array(); - } - if (isset($form_state['node'])) { - $node = (object) ($form_state['node'] + (array) $node); - } + $node = entity_form_initialize($form, $form_state, 'node', $node); + if (isset($form_state['node_preview'])) { $form['#prefix'] = $form_state['node_preview']; } @@ -141,6 +142,9 @@ function node_form($form, &$form_state, $form['title']['#weight'] = -5; } + // @todo Legacy support. Modules adding form building and processing functions + // to the node form are encouraged to access the node using + // $form_state['entity']. Remove in Drupal 8. $form['#node'] = $node; $form['additional_settings'] = array( @@ -278,7 +282,6 @@ function node_form($form, &$form_state, } $form['#validate'][] = 'node_form_validate'; - $form['#builder_function'] = 'node_form_submit_build_node'; field_attach_form('node', $node, $form, $form_state, $node->language); return $form; @@ -293,14 +296,15 @@ function node_form_delete_submit($form, $destination = drupal_get_destination(); unset($_GET['destination']); } - $node = $form['#node']; + $node = $form_state['entity']; $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination)); } function node_form_build_preview($form, &$form_state) { - $node = node_form_submit_build_node($form, $form_state); + $node = entity_form_submit($form, $form_state); $form_state['node_preview'] = node_preview($node); + $form_state['rebuild'] = TRUE; } /** @@ -382,7 +386,7 @@ function theme_node_preview($variables) } function node_form_submit($form, &$form_state) { - $node = node_form_submit_build_node($form, $form_state); + $node = entity_form_submit($form, $form_state); $insert = empty($node->nid); node_save($node); $node_link = l(t('view'), 'node/' . $node->nid); @@ -398,7 +402,6 @@ function node_form_submit($form, &$form_ drupal_set_message(t('@type %title has been updated.', $t_args)); } if ($node->nid) { - unset($form_state['rebuild']); $form_state['values']['nid'] = $node->nid; $form_state['nid'] = $node->nid; $form_state['redirect'] = 'node/' . $node->nid; @@ -407,29 +410,13 @@ function node_form_submit($form, &$form_ // In the unlikely case something went wrong on save, the node will be // rebuilt and node form redisplayed the same way as in preview. drupal_set_message(t('The post could not be saved.'), 'error'); + $form_state['rebuild'] = TRUE; } // Clear the page and block caches. cache_clear_all(); } /** - * Build a node by processing submitted form values and prepare for a form rebuild. - */ -function node_form_submit_build_node($form, &$form_state) { - // Unset any button-level handlers, execute all the form-level submit - // functions to process the form values into an updated node. - unset($form_state['submit_handlers']); - form_execute_handlers('submit', $form, $form_state); - $node = node_submit((object) $form_state['values']); - - field_attach_submit('node', $node, $form, $form_state); - - $form_state['node'] = (array) $node; - $form_state['rebuild'] = TRUE; - return $node; -} - -/** * Menu callback -- ask for confirmation of node deletion */ function node_delete_confirm($form, &$form_state, $node) { Index: modules/path/path.module =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.module,v retrieving revision 1.183 diff -u -p -r1.183 path.module --- modules/path/path.module 13 Feb 2010 21:41:58 -0000 1.183 +++ modules/path/path.module 21 May 2010 00:06:09 -0000 @@ -99,10 +99,10 @@ function path_menu() { function path_form_alter(&$form, $form_state, $form_id) { if (!empty($form['#node_edit_form'])) { $path = array(); - if (!empty($form['#node']->nid)) { - $conditions = array('source' => 'node/' . $form['#node']->nid); - if ($form['#node']->language != LANGUAGE_NONE) { - $conditions['language'] = $form['#node']->language; + if (!empty($form_state['entity']->nid)) { + $conditions = array('source' => 'node/' . $form_state['entity']->nid); + if ($form_state['entity']->language != LANGUAGE_NONE) { + $conditions['language'] = $form_state['entity']->language; } $path = path_load($conditions); if ($path === FALSE) { @@ -111,9 +111,9 @@ function path_form_alter(&$form, $form_s } $path += array( 'pid' => NULL, - 'source' => isset($form['#node']->nid) ? 'node/' . $form['#node']->nid : NULL, + 'source' => isset($form_state['entity']->nid) ? 'node/' . $form_state['entity']->nid : NULL, 'alias' => '', - 'language' => isset($form['#node']->language) ? $form['#node']->language : LANGUAGE_NONE, + 'language' => isset($form_state['entity']->language) ? $form_state['entity']->language : LANGUAGE_NONE, ); $form['path'] = array( Index: modules/poll/poll.module =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v retrieving revision 1.348 diff -u -p -r1.348 poll.module --- modules/poll/poll.module 9 May 2010 13:37:32 -0000 1.348 +++ modules/poll/poll.module 21 May 2010 00:06:09 -0000 @@ -352,15 +352,18 @@ function poll_form($node, &$form_state) * return just the changed part of the form. */ function poll_more_choices_submit($form, &$form_state) { - include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc'; - // Set the form to rebuild and run submit handlers. - node_form_submit_build_node($form, $form_state); - // Make the changes we want to the form state. if ($form_state['values']['poll_more']) { $n = $_GET['q'] == 'system/ajax' ? 1 : 5; $form_state['choice_count'] = count($form_state['values']['choice']) + $n; } + // Renumber the choices. This invalidates the corresponding key/value + // associations in $form_state['input'], so clear that out. This requires + // poll_form() to rebuild the choices with the values in + // $form_state['entity']->choice, which it does. + $form_state['entity']->choice = array_values($form_state['values']['choice']); + unset($form_state['input']['choice']); + $form_state['rebuild'] = TRUE; } function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight = 0, $size = 10) { Index: modules/simpletest/tests/form_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v retrieving revision 1.39 diff -u -p -r1.39 form_test.module --- modules/simpletest/tests/form_test.module 28 Apr 2010 16:11:22 -0000 1.39 +++ modules/simpletest/tests/form_test.module 21 May 2010 00:06:10 -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 21 May 2010 00:06:10 -0000 @@ -90,6 +90,10 @@ function hook_hook_info_alter(&$hooks) { * - uri callback: A function taking an entity as argument and returning the * uri elements of the entity, e.g. 'path' and 'options'. The actual entity * uri can be constructed by passing these elements to url(). + * - submit callback: A function that acts on an entity after a form + * submission if the form submit handler requires an updated entity (e.g., + * in response to a "Save" or "Preview" button). See entity_form_submit(). + * This callback only updates the * - fieldable: Set to TRUE if you want your entity type to be fieldable. * - entity keys: An array describing how the Field API can extract the * information it needs from the objects of the type. Elements: @@ -151,7 +155,8 @@ function hook_entity_info() { 'controller class' => 'NodeController', 'base table' => 'node', 'revision table' => 'node_revision', - 'path callback' => 'node_path', + 'uri callback' => 'node_uri', + 'submit callback' => 'node_submit', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'nid', @@ -242,6 +247,33 @@ function hook_entity_load($entities, $ty } /** + * Act on entities after a form submission, before the entity is saved, previewed, or used for building the next step in a multi-step form. + * + * Modules may use this hook to extract their data from $form_state and set it + * on $entity. Note that every key/value pair in $form_state['values'] is + * automatically added as a corresponding property/value pair in $entity, so + * modules only need to implement this hook to do something different than that. + * + * @param $entity + * The entity being updated from data in $form_state. + * @param $type + * The type of entity being updated (i.e. node, user, comment). + * @param $form + * The form being used to edit the entity. + * @param $form_state + * The form state array. + * + * @see entity_form_submit() + */ +function hook_entity_submit($entity, $type, $form, $form_state) { + // Decompose the selected menu parent option into 'menu_name' and 'plid', if + // the form used the default parent selection widget. + if ($entity_type == 'node' && !empty($form_state['values']['menu']['parent'])) { + list($entity->menu['menu_name'], $entity->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + } +} + +/** * Act on entities when inserted. * * Generic insert hook called for all entity types via entity_invoke(). @@ -1255,7 +1287,8 @@ function hook_page_alter(&$page) { * Perform alterations before a form is rendered. * * One popular use of this hook is to add form elements to the node form. When - * altering a node form, the node object can be accessed at $form['#node']. + * altering a node form, the node object can be accessed at + * $form_state['entity']. * * Note that instead of hook_form_alter(), which is called for all forms, you * can also use hook_form_FORM_ID_alter() to alter a specific form. For each Index: modules/taxonomy/taxonomy.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.admin.inc,v retrieving revision 1.105 diff -u -p -r1.105 taxonomy.admin.inc --- modules/taxonomy/taxonomy.admin.inc 13 May 2010 07:53:02 -0000 1.105 +++ modules/taxonomy/taxonomy.admin.inc 21 May 2010 00:06:10 -0000 @@ -630,20 +630,16 @@ function taxonomy_form_term($form, &$for 'weight' => 0, ); - // Take into account multi-step rebuilding. - if (isset($form_state['term'])) { - $edit = $form_state['term'] + $edit; - } + $term = entity_form_initialize($form, $form_state, 'taxonomy_term', (object) $edit); - $parent = array_keys(taxonomy_get_parents($edit['tid'])); - $form['#term'] = $edit; + $parent = array_keys(taxonomy_get_parents($term->tid)); + $form['#term'] = (array) $term; $form['#term']['parent'] = $parent; $form['#vocabulary'] = $vocabulary; - $form['#builder_function'] = 'taxonomy_form_term_submit_builder'; // Check for confirmation forms. if (isset($form_state['confirm_delete'])) { - return array_merge($form, taxonomy_term_confirm_delete($form, $form_state, $edit['tid'])); + return array_merge($form, taxonomy_term_confirm_delete($form, $form_state, $term->tid)); } elseif (isset($form_state['confirm_parents'])) { return array_merge($form, taxonomy_term_confirm_parents($form, $form_state, $vocabulary)); @@ -652,7 +648,7 @@ function taxonomy_form_term($form, &$for $form['name'] = array( '#type' => 'textfield', '#title' => t('Name'), - '#default_value' => $edit['name'], + '#default_value' => $term->name, '#maxlength' => 255, '#required' => TRUE, '#weight' => -5, @@ -660,18 +656,18 @@ function taxonomy_form_term($form, &$for $form['description'] = array( '#type' => 'text_format', '#title' => t('Description'), - '#default_value' => $edit['description'], - '#format' => $edit['format'], + '#default_value' => $term->description, + '#format' => $term->format, '#weight' => 0, ); $form['vocabulary_machine_name'] = array( '#type' => 'textfield', '#access' => FALSE, - '#value' => isset($edit['vocabulary_machine_name']) ? $edit['vocabulary_machine_name'] : $vocabulary->name, + '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name, ); - field_attach_form('taxonomy_term', (object) $edit, $form, $form_state); + field_attach_form('taxonomy_term', $term, $form, $form_state); $form['relations'] = array( '#type' => 'fieldset', @@ -686,23 +682,23 @@ function taxonomy_form_term($form, &$for // full vocabulary. Contrib modules can then intercept before // hook_form_alter to provide scalable alternatives. if (!variable_get('taxonomy_override_selector', FALSE)) { - $parent = array_keys(taxonomy_get_parents($edit['tid'])); - $children = taxonomy_get_tree($vocabulary->vid, $edit['tid']); + $parent = array_keys(taxonomy_get_parents($term->tid)); + $children = taxonomy_get_tree($vocabulary->vid, $term->tid); // A term can't be the child of itself, nor of its children. foreach ($children as $child) { $exclude[] = $child->tid; } - $exclude[] = $edit['tid']; + $exclude[] = $term->tid; $tree = taxonomy_get_tree($vocabulary->vid); $options = array('<' . t('root') . '>'); if (empty($parent)) { $parent = array(0); } - foreach ($tree as $term) { - if (!in_array($term->tid, $exclude)) { - $options[$term->tid] = str_repeat('-', $term->depth) . $term->name; + foreach ($tree as $item) { + if (!in_array($item->tid, $exclude)) { + $options[$item->tid] = str_repeat('-', $item->depth) . $item->name; } } $form['relations']['parent'] = array( @@ -718,7 +714,7 @@ function taxonomy_form_term($form, &$for '#type' => 'textfield', '#title' => t('Weight'), '#size' => 6, - '#default_value' => $edit['weight'], + '#default_value' => $term->weight, '#description' => t('Terms are displayed in ascending order by weight.'), '#required' => TRUE, ); @@ -728,7 +724,7 @@ function taxonomy_form_term($form, &$for ); $form['tid'] = array( '#type' => 'value', - '#value' => $edit['tid'], + '#value' => $term->tid, ); $form['actions'] = array('#type' => 'actions'); @@ -738,7 +734,7 @@ function taxonomy_form_term($form, &$for '#weight' => 5, ); - if ($edit['tid']) { + if ($term->tid) { $form['actions']['delete'] = array( '#type' => 'submit', '#value' => t('Delete'), @@ -759,7 +755,11 @@ function taxonomy_form_term($form, &$for * @see taxonomy_form_term() */ function taxonomy_form_term_validate($form, &$form_state) { - field_attach_form_validate('taxonomy_term', (object) $form_state['values'], $form, $form_state); + // See http://drupal.org/node/367006#comment-2601522 for why we only validate + // a pseudo term object rather than $form_state['entity']. + $term = (object) $form_state['values']; + + field_attach_form_validate('taxonomy_term', $term, $form, $form_state); // Ensure numeric values. if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) { @@ -790,7 +790,7 @@ function taxonomy_form_term_submit($form return; } - $term = taxonomy_form_term_submit_builder($form, $form_state); + $term = entity_form_submit($form, $form_state); $status = taxonomy_term_save($term); switch ($status) { @@ -828,27 +828,6 @@ function taxonomy_form_term_submit($form $form_state['values']['tid'] = $term->tid; $form_state['tid'] = $term->tid; - // Do not rebuild here. The term is saved by now and the form should clear. - $form_state['rebuild'] = FALSE; -} - -/** - * Build a term by processing form values and prepare for a form rebuild. - */ -function taxonomy_form_term_submit_builder($form, &$form_state) { - $term = (object) $form_state['values']; - - // Convert text_format field into values expected by taxonomy_term_save(). - $description = $form_state['values']['description']; - $term->description = $description['value']; - $term->format = $description['format']; - - field_attach_submit('taxonomy_term', $term, $form, $form_state); - - $form_state['term'] = (array) $term; - $form_state['rebuild'] = TRUE; - - return $term; } /** Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.590 diff -u -p -r1.590 taxonomy.module --- modules/taxonomy/taxonomy.module 14 May 2010 04:41:54 -0000 1.590 +++ modules/taxonomy/taxonomy.module 21 May 2010 00:06:10 -0000 @@ -88,6 +88,7 @@ function taxonomy_entity_info() { 'controller class' => 'TaxonomyTermController', 'base table' => 'taxonomy_term_data', 'uri callback' => 'taxonomy_term_uri', + 'submit callback' => 'taxonomy_term_submit', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'tid', @@ -453,6 +454,16 @@ function taxonomy_check_vocabulary_hiera } /** + * Entity submit callback. + */ +function taxonomy_term_submit($term, $form, &$form_state) { + // Convert text_format field into values expected by taxonomy_term_save(). + $description = $form_state['values']['description']; + $term->description = $description['value']; + $term->format = $description['format']; +} + +/** * Save a term object to the database. * * @param $term Index: modules/translation/translation.module =================================================================== RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v retrieving revision 1.79 diff -u -p -r1.79 translation.module --- modules/translation/translation.module 16 Apr 2010 13:52:23 -0000 1.79 +++ modules/translation/translation.module 21 May 2010 00:06:10 -0000 @@ -124,8 +124,8 @@ function translation_form_node_type_form * is about to be created. */ function translation_form_alter(&$form, &$form_state, $form_id) { - if (!empty($form['#node_edit_form']) && translation_supported_type($form['#node']->type)) { - $node = $form['#node']; + if (!empty($form['#node_edit_form']) && translation_supported_type($form_state['entity']->type)) { + $node = $form_state['entity']; if (!empty($node->translation_source)) { // We are creating a translation. Add values and lock language field. $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source); @@ -315,10 +315,12 @@ function translation_node_update($node) * * Ensure that duplicate translations can not be created for the same source. */ -function translation_node_validate($node, $form) { +function translation_node_validate($node, $form, $form_state) { // Only act on translatable nodes with a tnid or translation_source. - if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) { - $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid; + // Use $form_state['entity'] instead of $node for information about the node + // not being edited by this form. + if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form_state['entity']->translation_source->nid))) { + $tnid = !empty($node->tnid) ? $node->tnid : $form_state['entity']->translation_source->nid; $translations = translation_node_get_translations($tnid); if (isset($translations[$node->language]) && $translations[$node->language]->nid != $node->nid ) { form_set_error('language', t('There is already a translation in this language.'));