Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1174 diff -u -p -r1.1174 common.inc --- includes/common.inc 26 May 2010 19:51:01 -0000 1.1174 +++ includes/common.inc 28 May 2010 02:50:09 -0000 @@ -6624,6 +6624,37 @@ function entity_invoke($op, $entity_type } /** + * 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 Index: modules/book/book.module =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.module,v retrieving revision 1.543 diff -u -p -r1.543 book.module --- modules/book/book.module 23 May 2010 19:10:22 -0000 1.543 +++ modules/book/book.module 28 May 2010 02:50:10 -0000 @@ -416,15 +416,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, ); } @@ -432,6 +430,22 @@ function book_form_alter(&$form, &$form_ } /** + * Submit handler to change a node's book. + * + * This handler is run when JavaScript is disabled. It triggers the form to + * rebuild so that the "Parent item" options are changed to reflect the newly + * selected book. When JavaScript is enabled, the submit button that triggers + * this handler is hidden, and the "Book" dropdown directly triggers the + * book_form_update() AJAX callback instead. + * + * @see book_form_update() + */ +function book_pick_book_nojs_submit($form, &$form_state) { + $form_state['node']->book = $form_state['values']['book']; + $form_state['rebuild'] = TRUE; +} + +/** * Build the parent selection form element for the node form or outline tab. * * This function is also called when generating a new set of options during the Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.878 diff -u -p -r1.878 comment.module --- modules/comment/comment.module 27 May 2010 12:29:39 -0000 1.878 +++ modules/comment/comment.module 28 May 2010 02:50:11 -0000 @@ -1744,23 +1744,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' => '', @@ -1771,7 +1756,28 @@ 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; + } + } + + // 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. @@ -1948,7 +1954,7 @@ 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 = $form['#builder_function']($form, $form_state); $form_state['comment_preview'] = comment_preview($comment); } @@ -2015,7 +2021,12 @@ function comment_preview($comment) { */ function comment_form_validate($form, &$form_state) { global $user; + + // $form_state['comment'] 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. $comment = (object) $form_state['values']; + field_attach_form_validate('comment', $comment, $form, $form_state); if (!empty($form_state['values']['cid'])) { @@ -2067,48 +2078,59 @@ function comment_form_validate($form, &$ /** * 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; } - $comment['created'] = strtotime($comment['date']); - $comment['changed'] = REQUEST_TIME; + if (empty($comment->date)) { + $comment->date = 'now'; + } - if (!empty($comment['name']) && ($account = user_load_by_name($comment['name']))) { - $comment['uid'] = $account->uid; + $comment->created = strtotime($comment->date); + $comment->changed = REQUEST_TIME; + + if (!empty($comment->name) && ($account = user_load_by_name($comment->name))) { + $comment->uid = $account->uid; } // Validate the comment's subject. If not specified, extract from comment body. - if (trim($comment['subject']) == '') { + if (trim($comment->subject) == '') { // The body may be in any format, so: // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. - $comment['subject'] = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment['comment_body'][LANGUAGE_NONE][0]['value'], $comment['comment_body'][LANGUAGE_NONE][0]['format'])))), 29, TRUE); + $comment->subject = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment->comment_body[LANGUAGE_NONE][0]['value'], $comment->comment_body[LANGUAGE_NONE][0]['format'])))), 29, TRUE); // Edge cases where the comment body is populated only by HTML tags will // require a default subject. - if ($comment['subject'] == '') { - $comment['subject'] = t('(No subject)'); + if ($comment->subject == '') { + $comment->subject = t('(No subject)'); } } - return (object) $comment; + 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; + comment_submit($comment); + // @todo Legacy support. Remove in Drupal 8, as it is better for each button's + // submit handler to decide whether to rebuild. $form_state['rebuild'] = TRUE; return $comment; } @@ -2118,7 +2140,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 = $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) { Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.88 diff -u -p -r1.88 field.attach.inc --- modules/field/field.attach.inc 23 May 2010 19:10:23 -0000 1.88 +++ modules/field/field.attach.inc 28 May 2010 02:50:11 -0000 @@ -556,9 +556,6 @@ function field_attach_form($entity_type, $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) { Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.36 diff -u -p -r1.36 field.default.inc --- modules/field/field.default.inc 23 May 2010 19:10:23 -0000 1.36 +++ modules/field/field.default.inc 28 May 2010 02:50:11 -0000 @@ -65,25 +65,12 @@ function field_default_validate($entity_ } 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); } /** Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.49 diff -u -p -r1.49 field.form.inc --- modules/field/field.form.inc 23 May 2010 07:30:56 -0000 1.49 +++ modules/field/field.form.inc 28 May 2010 02:50:11 -0000 @@ -359,17 +359,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.10 diff -u -p -r1.10 field_test.entity.inc --- modules/field/tests/field_test.entity.inc 23 May 2010 19:10:23 -0000 1.10 +++ modules/field/tests/field_test.entity.inc 28 May 2010 02:50:11 -0000 @@ -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 * 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($ 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; } 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 28 May 2010 02:50:12 -0000 @@ -596,7 +596,6 @@ function menu_form_alter(&$form, $form_s 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_s } /** - * 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']); } } Index: modules/node/node.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v retrieving revision 1.68 diff -u -p -r1.68 node.api.php --- modules/node/node.api.php 23 May 2010 19:10:23 -0000 1.68 +++ modules/node/node.api.php 28 May 2010 02:50:12 -0000 @@ -681,6 +681,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 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 28 May 2010 02:50:12 -0000 @@ -76,6 +76,9 @@ 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); @@ -89,14 +92,13 @@ 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); - } + // 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']; } @@ -299,7 +301,7 @@ function node_form_delete_submit($form, 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); } @@ -382,7 +384,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); @@ -413,18 +415,37 @@ function node_form_submit($form, &$form_ } /** - * 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 must run every time the form state's node entity is + // updated regardless which button ("Save", "Preview", etc.) triggered the + // submission. Remove this in Drupal 8. Module authors are encouraged to use + // hook_node_submit(). 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; + node_submit($node); + foreach (module_implements('node_submit') as $module) { + $function = $module . '_node_submit'; + $function($node, $form, $form_state); + } + + // @todo Legacy support. Remove in Drupal 8, as it is better for each button's + // submit handler to decide whether to rebuild. $form_state['rebuild'] = TRUE; return $node; } Index: modules/poll/poll.module =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v retrieving revision 1.349 diff -u -p -r1.349 poll.module --- modules/poll/poll.module 23 May 2010 19:10:23 -0000 1.349 +++ modules/poll/poll.module 28 May 2010 02:50:12 -0000 @@ -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) { 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 28 May 2010 02:50:12 -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/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 28 May 2010 02:50:13 -0000 @@ -630,20 +630,21 @@ function taxonomy_form_term($form, &$for 'weight' => 0, ); - // Take into account multi-step rebuilding. - if (isset($form_state['term'])) { - $edit = $form_state['term'] + $edit; - } + // 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. + $form_state += array('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 +653,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 +661,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 +687,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 +719,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 +729,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 +739,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,6 +760,9 @@ function taxonomy_form_term($form, &$for * @see taxonomy_form_term() */ function taxonomy_form_term_validate($form, &$form_state) { + // $form_state['term'] 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. field_attach_form_validate('taxonomy_term', (object) $form_state['values'], $form, $form_state); // Ensure numeric values. @@ -790,7 +794,7 @@ function taxonomy_form_term_submit($form 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 +837,21 @@ function taxonomy_form_term_submit($form } /** - * 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; + // @todo Legacy support. Remove in Drupal 8, as it is better for each button's + // submit handler to decide whether to rebuild. $form_state['rebuild'] = TRUE; - return $term; }