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 20 May 2010 03:43:44 -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['node']; $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['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.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 20 May 2010 03:43:44 -0000 @@ -1138,7 +1138,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['node']; $form['comment_settings'] = array( '#type' => 'fieldset', '#access' => user_access('administer comments'), 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 20 May 2010 03:43:45 -0000 @@ -368,17 +368,22 @@ 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. + // @todo Legacy entity forms assume that #builder_function is called from all + // button-level submit handlers that trigger a rebuild. This is poor + // encapsulation and can lead to security vulnerabilities when used in + // conjunction with #limit_validation_errors. Node forms have been fixed, + // but some entity forms still rely on this. Remove this once all entity + // forms have been fixed: http://drupal.org/node/735800. 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]); - } + $form['#builder_function']($form, $form_state); + } + // Make the changes we want to the form state and trigger a rebuild. + $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/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 20 May 2010 03:43:46 -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['node']->type) && locale_multilingual_node_type($form_state['node']->type)) { $form['language'] = array( '#type' => 'select', '#title' => t('Language'), - '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''), + '#default_value' => (isset($form_state['node']->language) ? $form_state['node']->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['node']->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 20 May 2010 03:43:46 -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['node']->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['node']->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() + * Implementation of 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.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 20 May 2010 03:43:46 -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['node'] 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.')); @@ -681,6 +687,37 @@ function hook_node_validate($node, $form } /** + * Update the node object based on submitted form values. + * + * This hook is called when the "Save" or "Preview" button of a node editing + * form is clicked. It may also be called during other button-clicks if the + * submit handler for that button requires an updated node object. + * + * Modules may use this hook to extract their data from $form_state and set it + * on the $node object. Note that every key/value pair in $form_state['values'] + * is automatically added as a corresponding property/value pair in $node, so + * modules only need to implement this hook to do something different than that. + * + * @param $node + * The node being built from data in $form_state. + * @param $form + * The form being used to edit the node. + * @param $form_state + * The form state array. + * + * @see node_form_submit_update_node() + * + * @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 @@ -1104,10 +1141,12 @@ function hook_update($node) { * The node being validated. * @param $form * The form being used to edit the node. + * @param $form_state + * The form state array. * * @ingroup node_api_hooks */ -function hook_validate($node, &$form) { +function hook_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 20 May 2010 03:43:46 -0000 @@ -907,7 +907,7 @@ function node_object_prepare($node) { /** * Perform validation checks on the given node. */ -function node_validate($node, $form = array()) { +function node_validate($node, $form = array(), &$form_state = NULL) { $type = node_type_get_type($node); if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) { @@ -927,15 +927,24 @@ function node_validate($node, $form = ar form_set_error('date', t('You have to specify a valid date.')); } - // Do node-type-specific validation checks. - node_invoke($node, 'validate', $form); - module_invoke_all('node_validate', $node, $form); + // Invoke node-type specific validation, field validation, and + // hook_node_validate(). Can't use node_invoke() or module_invoke_all(), + // because the invoked functions need to receive $form_state by reference. + $function = node_type_get_base($node) . '_validate'; + if (function_exists($function)) { + $function($node, $form, $form_state); + } + 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); + } } /** - * Prepare node for saving by populating author and creation date. + * Prepare a node for saving or previewing after its editing form has been submitted. */ -function node_submit($node) { +function node_submit($node, $form = array(), &$form_state = NULL) { global $user; // A user might assign the node author by entering a user name in the node @@ -952,6 +961,23 @@ function node_submit($node) { $node->created = !empty($node->date) ? strtotime($node->date) : REQUEST_TIME; $node->validated = TRUE; + // Invoke node-type specific and field-level submit handlers, and + // hook_node_submit(). Can't use node_invoke() or module_invoke_all(), + // because the invoked functions need to receive $form_state by reference. + // Support legacy modules that call node_submit() without passing $form and + // $form_state and that do not expect the extra hooks to be called. + if (isset($form_state)) { + $function = node_type_get_base($node) . '_submit'; + if (function_exists($function)) { + $function($node, $form, $form_state); + } + field_attach_submit('node', $node, $form, $form_state); + foreach (module_implements('node_submit') as $module) { + $function = $module . '_node_submit'; + $function($node, $form, $form_state); + } + } + return $node; } 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 20 May 2010 03:43:46 -0000 @@ -75,13 +75,21 @@ function node_add($type) { return $output; } +/** + * Validate the node add/edit form. + */ function node_form_validate($form, &$form_state) { + // @todo node_submit() and the hooks that it calls receive the full node + // as the $node parameter. For consistency and to enable advanced form + // workflows and validation, it would be ideal for the same to be true for + // node_validate(). However, there are various obstacles to this, and until + // they're resolved, we pass a pseudo node object that only contains the + // data being edited. See http://drupal.org/node/367006#comment-2601522. $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); + // This performs default validation, field validation, and calls + // hook_node_validate(). + node_validate($node, $form, $form_state); } /** @@ -89,14 +97,17 @@ 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 the form is being rebuilt after a submit handler updated + // $form_state['node'], use that. Otherwise, initialize $form_state['node'] + // with the node object passed as a build argument. if (isset($form_state['node'])) { - $node = (object) ($form_state['node'] + (array) $node); + $node = $form_state['node']; } + else { + $form_state['node'] = $node; + } + if (isset($form_state['node_preview'])) { $form['#prefix'] = $form_state['node_preview']; } @@ -141,7 +152,8 @@ function node_form($form, &$form_state, $form['title']['#weight'] = -5; } - $form['#node'] = $node; + // @todo Legacy support. Remove in Drupal 8. + $form['#node'] = $form_state['node']; $form['additional_settings'] = array( '#type' => 'vertical_tabs', @@ -278,7 +290,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 +304,15 @@ function node_form_delete_submit($form, $destination = drupal_get_destination(); unset($_GET['destination']); } - $node = $form['#node']; + $node = $form_state['node']; $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); + node_form_submit_update_node($form, $form_state); + $form_state['node_preview'] = node_preview($form_state['node']); + $form_state['rebuild'] = TRUE; } /** @@ -382,7 +394,8 @@ function theme_node_preview($variables) } function node_form_submit($form, &$form_state) { - $node = node_form_submit_build_node($form, $form_state); + node_form_submit_update_node($form, $form_state); + $node = $form_state['node']; $insert = empty($node->nid); node_save($node); $node_link = l(t('view'), 'node/' . $node->nid); @@ -398,7 +411,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,26 +419,58 @@ 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. + * Update the node object represented by the form. + * + * This function is called by submit handlers of buttons on a node editing form + * that require the node object represented by the form to be updated based on + * the submitted form values. For example, the submit handler of the "Save" + * button requires an updated node object to save to the database, and the + * submit handler of the "Preview" button requires an updated node object to + * preview. Other modules may add additional buttons with submit handlers that + * similarly require an updated node object. + * + * This function invokes submit handlers that 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 + * node object themselves with data that has been validated (see + * poll_more_choices_submit()) or not update the node object 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['node'] contains the node object that + * needs to be updated. */ -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. +function node_form_submit_update_node($form, &$form_state) { + // @todo Legacy support for modules that use a form-level submit handler to + // extend the node being built. For the node to be built properly during a + // multistep 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_node_submit(). Remove this in Drupal 8. 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); + // The node 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 node with the new values. + $node = $form_state['node']; + foreach ($form_state['values'] as $key => $value) { + $node->$key = $value; + } - $form_state['node'] = (array) $node; - $form_state['rebuild'] = TRUE; - return $node; + // This invokes all the necessary hooks to allow modules to customize the node + // beyond the simple mapping done above. + node_submit($node, $form, $form_state); } /** 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 20 May 2010 03:43:47 -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['node']->nid)) { + $conditions = array('source' => 'node/' . $form_state['node']->nid); + if ($form_state['node']->language != LANGUAGE_NONE) { + $conditions['language'] = $form_state['node']->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['node']->nid) ? 'node/' . $form_state['node']->nid : NULL, 'alias' => '', - 'language' => isset($form['#node']->language) ? $form['#node']->language : LANGUAGE_NONE, + 'language' => isset($form_state['node']->language) ? $form_state['node']->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 20 May 2010 03:43:47 -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['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/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 20 May 2010 03:43:48 -0000 @@ -1255,7 +1255,7 @@ 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['node']. * * 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/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 20 May 2010 03:43:48 -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['node']->type)) { + $node = $form_state['node']; 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['node'] 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['node']->translation_source->nid))) { + $tnid = !empty($node->tnid) ? $node->tnid : $form_state['node']->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.'));