diff --git modules/book/book.module modules/book/book.module index eeccb9d..8888ba5 100644 --- modules/book/book.module +++ modules/book/book.module @@ -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_state, $form_id) { 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_state, $form_id) { } /** + * 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 diff --git modules/field/field.form.inc modules/field/field.form.inc index 37858d0..51a04ca 100644 --- modules/field/field.form.inc +++ modules/field/field.form.inc @@ -368,17 +368,22 @@ function field_default_form_errors($entity_type, $entity, $field, $instance, $la * 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; } /** diff --git modules/menu/menu.module modules/menu/menu.module index d72fbdd..098d345 100644 --- modules/menu/menu.module +++ modules/menu/menu.module @@ -596,7 +596,6 @@ function menu_form_alter(&$form, $form_state, $form_id) { 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_state, $form_id) { } /** - * 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']); } } diff --git modules/node/node.api.php modules/node/node.api.php index 81daab5..6831147 100644 --- modules/node/node.api.php +++ modules/node/node.api.php @@ -681,6 +681,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" buttons 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_build_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 diff --git modules/node/node.pages.inc modules/node/node.pages.inc index d92feae..73f2348 100644 --- modules/node/node.pages.inc +++ modules/node/node.pages.inc @@ -75,8 +75,14 @@ function node_add($type) { return $output; } +/** + * Validate the node add/edit form. + */ function node_form_validate($form, &$form_state) { - $node = (object)$form_state['values']; + // @todo Legacy support. For Drupal 8, implement a clear and consistent + // distinction between form validation and entity validation, rather than + // casting arbitrary form values to mock entities. + $node = (object) $form_state['values']; node_validate($node, $form); // Field validation. Requires access to $form_state, so this cannot be @@ -89,14 +95,17 @@ function node_form_validate($form, &$form_state) { */ 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 +150,8 @@ function node_form($form, &$form_state, $node) { $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', @@ -279,7 +289,6 @@ function node_form($form, &$form_state, $node) { $form['#validate'][] = 'node_form_validate'; $form['#theme'] = array($node->type . '_node_form', 'node_form'); - $form['#builder_function'] = 'node_form_submit_build_node'; field_attach_form('node', $node, $form, $form_state, $node->language); return $form; @@ -302,6 +311,7 @@ function node_form_delete_submit($form, &$form_state) { function node_form_build_preview($form, &$form_state) { $node = node_form_submit_build_node($form, $form_state); $form_state['node_preview'] = node_preview($node); + $form_state['rebuild'] = TRUE; } /** @@ -399,7 +409,6 @@ function node_form_submit($form, &$form_state) { 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; @@ -408,25 +417,42 @@ function node_form_submit($form, &$form_state) { // 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. + * Build a node by processing the submitted form values. + * + * 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. */ 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 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']); + // Execute all entity-level and field-level submit hooks. + $node = (object) ($form_state['values'] + (array) $form_state['node']); + $node = node_submit($node); field_attach_submit('node', $node, $form, $form_state); + foreach (module_implements('node_submit') as $module) { + $function = $module . '_node_submit'; + $function($node, $form, $form_state); + } - $form_state['node'] = (array)$node; - $form_state['rebuild'] = TRUE; + // There may be additional submit handlers that run after the one that calls + // this function, or one of the submit handlers may trigger a form rebuild, so + // store the built node in $form_state in addition to returning it. + $form_state['node'] = $node; return $node; } diff --git modules/poll/poll.module modules/poll/poll.module index 26004e2..3dabb27 100644 --- modules/poll/poll.module +++ modules/poll/poll.module @@ -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) {