Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.190 diff -u -p -r1.190 form.inc --- includes/form.inc 24 Apr 2007 13:53:10 -0000 1.190 +++ includes/form.inc 25 Apr 2007 01:43:31 -0000 @@ -27,92 +27,94 @@ */ /** - * Retrieves a form from a builder function, passes it on for - * processing, and renders the form or redirects to its destination - * as appropriate. In multi-step form scenarios, it handles properly - * processing the values using the previous step's form definition, - * then rendering the requested step for display. + * Retrieves a form from a constructor function, or from the cache if + * the form was built in a previous page-load. The form is then passesed + * on for processing, after and rendered for display if necessary. * * @param $form_id * The unique string identifying the desired form. If a function * with that name exists, it is called to build the form array. * Modules that need to generate the same form (or very similar forms) * using different $form_ids can implement hook_forms(), which maps - * different $form_id values to the proper form building function. Examples + * different $form_id values to the proper form constructor function. Examples * may be found in node_forms(), search_forms(), and user_forms(). * @param ... - * Any additional arguments needed by the form building function. + * Any additional arguments needed by the form constructor function. * @return * The rendered form. */ function drupal_get_form($form_id) { - // In multi-step form scenarios, the incoming $_POST values are not - // necessarily intended for the current form. We need to build - // a copy of the previously built form for validation and processing, - // then go on to the one that was requested if everything works. - - $form_build_id = md5(mt_rand()); - if (isset($_POST['form_build_id']) && isset($_SESSION['form'][$_POST['form_build_id']]['args']) && $_POST['form_id'] == $form_id) { - // There's a previously stored multi-step form. We should handle - // IT first. - $stored = TRUE; - $args = $_SESSION['form'][$_POST['form_build_id']]['args']; - $form = call_user_func_array('drupal_retrieve_form', $args); - $form['#build_id'] = $_POST['form_build_id']; - } - else { - // We're coming in fresh; build things as they would be. If the - // form's #multistep flag is set, store the build parameters so - // the same form can be reconstituted for validation. - $args = func_get_args(); - $form = call_user_func_array('drupal_retrieve_form', $args); - if (isset($form['#multistep']) && $form['#multistep']) { - // Clean up old multistep form session data. - _drupal_clean_form_sessions(); - $_SESSION['form'][$form_build_id] = array('timestamp' => time(), 'args' => $args); - $form['#build_id'] = $form_build_id; - } - $stored = FALSE; - } - - // Process the form, submit it, and store any errors if necessary. - drupal_process_form($args[0], $form); - - if ($stored && !form_get_errors()) { - // If it's a stored form and there were no errors, we processed the - // stored form successfully. Now we need to build the form that was - // actually requested. We always pass in the current $_POST values - // to the builder function, as values from one stage of a multistep - // form can determine how subsequent steps are displayed. - $args = func_get_args(); - $args[] = $_POST; - $form = call_user_func_array('drupal_retrieve_form', $args); - unset($_SESSION['form'][$_POST['form_build_id']]); - if (isset($form['#multistep']) && $form['#multistep']) { - $_SESSION['form'][$form_build_id] = array('timestamp' => time(), 'args' => $args); - $form['#build_id'] = $form_build_id; - } - drupal_prepare_form($args[0], $form); - } + $form_state = array('storage' => NULL); + $expire = max(ini_get('session.cookie_lifetime'), 86400); - return drupal_render_form($args[0], $form); -} - - -/** - * Remove form information that's at least a day old from the - * $_SESSION['form'] array. - */ -function _drupal_clean_form_sessions() { - if (isset($_SESSION['form'])) { - foreach ($_SESSION['form'] as $build_id => $data) { - if ($data['timestamp'] < (time() - 84600)) { - unset($_SESSION['form'][$build_id]); + // If the incoming $_POST contains a form_build_id, we'll check the + // cache for a copy of the form in question. If it's there, we don't + // have to rebuild the form to proceed. In addition, if there is stored + // form_state data from a previous step, we'll retrieve it so it can + // be passed on to the form processing code. + if (isset($_POST['form_id']) && $_POST['form_id'] == $form_id && !empty($_POST['form_build_id'])) { + if ($cached = cache_get('form_'. $_POST['form_build_id'], 'cache_form')) { + $form = unserialize($cached->data); + if ($cached = cache_get('storage_'. $_POST['form_build_id'], 'cache_form')) { + $form_state['storage'] = unserialize($cached->data); } } } -} + // If the previous bit of code didn't result in a populated $form + // object, we're hitting the form for the first time and we need + // to build it from scratch. + $args = func_get_args(); + if (!isset($form)) { + $form = call_user_func_array('drupal_retrieve_form', $args); + $form_build_id = md5(mt_rand()); + $form['#build_id'] = $form_build_id; + drupal_prepare_form($form_id, $form, $form_state['storage']); + if (!empty($form['#cache'])) { + cache_set('form_'. $form_build_id, serialize($form), 'cache_form', $expire); + } + } + $form['#post'] = $_POST; + + // Now that we know we have a form, we'll process it (validating, + // submitting, and handling the results returned by its submission + // handlers. Submit handlers accumulate data in the form_state by + // altering the $form_state variable, which is passed into them by + // reference. + drupal_process_form($form_id, $form, $form_state); + + // If $form_state['storage'] is set by a submit handler, we can + // assume that the form's workflow is NOT complete, and that it + // needs to build a second, or third, or fourth, etc. step to + // display to the user. We'll add the storage element to the end + // of the args that were passed into drupal_get_form(), and call + // the constructor function to build this next step. We'll also + // cache the form and the $form_state['storage'] data so that + // we can properly resume at this point when it's next submitted. + if (isset($form_state['storage'])) { + $args[] = $form_state['storage']; + $form = call_user_func_array('drupal_retrieve_form', $args); + + // We need a new build_id for this new version of the form. + $form_build_id = md5(mt_rand()); + $form['#build_id'] = $form_build_id; + drupal_prepare_form($form_id, $form, $form_state['storage']); + + cache_set('storage_'. $form_build_id, serialize($form_state['storage']), 'cache_form', $expire); + cache_set('form_'. $form_build_id, serialize($form), 'cache_form', $expire); + + // Clear out all post data, as we don't want the previous step's + // data to pollute this one and trigger validate/submit handling, + // then process the form for rendering. + $_POST = array(); + $form['#post'] = array(); + drupal_process_form($form_id, $form, $form_state); + } + + // If we haven't redirected to a new location by now, we want to + // render whatever form array is currently in hand. + return drupal_render_form($form_id, $form); +} /** * Retrieves a form using a form_id, populates it with $form_values, @@ -124,43 +126,52 @@ function _drupal_clean_form_sessions() { * with that name exists, it is called to build the form array. * Modules that need to generate the same form (or very similar forms) * using different $form_ids can implement hook_forms(), which maps - * different $form_id values to the proper form building function. Examples + * different $form_id values to the proper form constructor function. Examples * may be found in node_forms(), search_forms(), and user_forms(). * @param $form_values * An array of values mirroring the values returned by a given form * when it is submitted by a user. + * @param $form_state + * A keyed array containing the current state of the form. This is + * used primarily by multi-step forms, and the exact contents of + * the array vary from form to form. After the form has been processed, + * $form_state will be populated with the results of the form's + * submission. * @param ... - * Any additional arguments needed by the form building function. - * @return - * Any form validation errors encountered. + * Any additional arguments needed by the form constructor function. * * For example: * * // register a new user + * $form_state = array(); * $values['name'] = 'robo-user'; * $values['mail'] = 'robouser@example.com'; * $values['pass'] = 'password'; - * drupal_execute('user_register', $values); + * drupal_execute('user_register', $values, $form_state); * * // Create a new node + * $form_state = array(); * $node = array('type' => 'story'); * $values['title'] = 'My node'; * $values['body'] = 'This is the body text!'; * $values['name'] = 'robo-user'; - * drupal_execute('story_node_form', $values, $node); + * drupal_execute('story_node_form', $values, $form_state, $node); */ -function drupal_execute($form_id, $form_values) { +function drupal_execute($form_id, $form_values, &$form_state) { $args = func_get_args(); - - $form_id = array_shift($args); - $form_values = array_shift($args); + + // We do a bit of juggling here because drupal_retrieve_form() expects + // the $form_state to be the last parameter, while drupal_execute() + // always takes it in as the third parameter. + $args = array_slice($args, 3); array_unshift($args, $form_id); - - if (isset($form_values)) { - $form = call_user_func_array('drupal_retrieve_form', $args); - $form['#post'] = $form_values; - return drupal_process_form($form_id, $form); + if (isset($form_state['storage'])) { + $args[] = $form_state['storage']; } + $form = call_user_func_array('drupal_retrieve_form', $args); + $form['#post'] = $form_values; + drupal_prepare_form($form_id, $form, $form_state['storage']); + drupal_process_form($form_id, $form, $form_state); } /** @@ -171,17 +182,17 @@ function drupal_execute($form_id, $form_ * with that name exists, it is called to build the form array. * Modules that need to generate the same form (or very similar forms) * using different $form_ids can implement hook_forms(), which maps - * different $form_id values to the proper form building function. + * different $form_id values to the proper form constructor function. * @param ... - * Any additional arguments needed by the form building function. + * Any additional arguments needed by the form constructor function. */ function drupal_retrieve_form($form_id) { static $forms; // We save two copies of the incoming arguments: one for modules to use - // when mapping form ids to builder functions, and another to pass to - // the builder function itself. We shift out the first argument -- the - // $form_id itself -- from the list to pass into the builder function, + // when mapping form ids to constructor functions, and another to pass to + // the constructor function itself. We shift out the first argument -- the + // $form_id itself -- from the list to pass into the constructor function, // since it's already known. $args = func_get_args(); $saved_args = $args; @@ -190,9 +201,9 @@ function drupal_retrieve_form($form_id) // We first check to see if there's a function named after the $form_id. // If there is, we simply pass the arguments on to it to get the form. if (!function_exists($form_id)) { - // In cases where many form_ids need to share a central builder function, + // In cases where many form_ids need to share a central constructor function, // such as the node editing form, modules can implement hook_forms(). It - // maps one or more form_ids to the correct builder functions. + // maps one or more form_ids to the correct constructor functions. // // We cache the results of that hook to save time, but that only works // for modules that know all their form_ids in advance. (A module that @@ -232,11 +243,15 @@ function drupal_retrieve_form($form_id) * The unique string identifying the current form. * @param $form * An associative array containing the structure of the form. - * @return - * The path to redirect the user to upon completion. + * @param $form_state + * A keyed array containing the current state of the form. This is + * used primarily by multi-step forms, and the exact contents of + * the array vary from form to form. After the form has been processed, + * $form_state will be populated with the results of the form's + * submission. */ -function drupal_process_form($form_id, &$form) { - global $form_values, $form_submitted, $user, $form_button_counter; +function drupal_process_form($form_id, &$form, &$form_state) { + global $form_values, $form_submitted, $user, $form_button_counter, $user; static $saved_globals = array(); // In some scenarios, this function can be called recursively. Pushing any pre-existing // $form_values and form submission data lets us start fresh without clobbering work done @@ -247,16 +262,41 @@ function drupal_process_form($form_id, & $form_submitted = FALSE; $form_button_counter = array(0, 0); - drupal_prepare_form($form_id, $form); - if (($form['#programmed']) || (!empty($_POST) && (($_POST['form_id'] == $form_id)))) { - drupal_validate_form($form_id, $form); - // IE does not send a button value when there is only one submit button (and no non-submit buttons) - // and you submit by pressing enter. - // In that case we accept a submission without button values. + $form = form_builder($form_id, $form); + if ((!empty($form['#programmed'])) || (!empty($form['#post']) && (($form['#post']['form_id'] == $form_id)))) { + $old_uid = $user->uid; + drupal_validate_form($form_id, $form, $form_state); + + // Normally, we look for the $form_submitted boolean that's set if + // the value of a submit button came in via the $_POST data. However, + // IE does not send a button value when there is only one submit + // button (and no non-submit buttons) and you submit by pressing enter. + // If the form was called by drupal_execute() and the calling function + // didn't specify an 'op' value, we'll see the same symptoms. To catch + // these special cases, we first check to see if the form is #programmed, + // then check to see if the $form_submitted boolean is set, then finally + // count the number of submit buttons and non-submit buttons in the form + // to test for IE's strange behavior. if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) { - $redirect = drupal_submit_form($form_id, $form); - if (!$form['#programmed']) { - drupal_redirect_form($form, $redirect); + $form_state['redirect'] = NULL; + drupal_submit_form($form, $form_state); + + // We'll clear out the cached copies of the form and its stored data + // here, as we've finished with them. The in-memory copies are still + // here, though. + if (variable_get('cache', CACHE_DISABLED) == CACHE_DISABLED || $user->uid) { + cache_clear_all('form_'. $old_uid .'_'. $form_values['form_build_id'], 'cache_form'); + cache_clear_all('storage_'. $old_uid .'_'. $form_values['form_build_id'], 'cache_form'); + } + + // If no submit handlers have populated the $form_state['storage'] + // bundle, we know we're finished and should redirect to a + // new destination page if one has been set. If the form was + // called by drupal_execute(), however, we'll skip this and let + // the calling function examine the resulting $form_state bundle + // itself. + if (!$form['#programmed'] && empty($form_state['storage'])) { + drupal_redirect_form($form, $form_state['redirect']); } } } @@ -264,9 +304,6 @@ function drupal_process_form($form_id, & // We've finished calling functions that alter the global values, so we can // restore the ones that were there before this function was called. list($form_values, $form_submitted, $form_button_counter) = array_pop($saved_globals); - if (isset($redirect)) { - return $redirect; - } } /** @@ -279,25 +316,21 @@ function drupal_process_form($form_id, & * theming, and hook_form_alter functions. * @param $form * An associative array containing the structure of the form. + * @param $storage + * The current persistent 'state' of the form, populated by the + * form's submission handlers and carried forward to subsequent + * builds. Passed in here so that hook_form_alter() calls can + * use it, as well. */ -function drupal_prepare_form($form_id, &$form) { +function drupal_prepare_form($form_id, &$form, $storage) { global $user; $form['#type'] = 'form'; if (!isset($form['#skip_duplicate_check'])) { $form['#skip_duplicate_check'] = FALSE; } + $form['#programmed'] = isset($form['#post']); - if (!isset($form['#post'])) { - $form['#post'] = $_POST; - $form['#programmed'] = FALSE; - } - else { - $form['#programmed'] = TRUE; - } - - // In multi-step form scenarios, this id is used to identify - // a unique instance of a particular form for retrieval. if (isset($form['#build_id'])) { $form['form_build_id'] = array( '#type' => 'hidden', @@ -330,7 +363,11 @@ function drupal_prepare_form($form_id, & if (isset($form_id)) { - $form['form_id'] = array('#type' => 'hidden', '#value' => $form_id, '#id' => form_clean_id("edit-$form_id")); + $form['form_id'] = array( + '#type' => 'hidden', + '#value' => $form_id, + '#id' => form_clean_id("edit-$form_id"), + ); } if (!isset($form['#id'])) { $form['#id'] = form_clean_id($form_id); @@ -340,20 +377,18 @@ function drupal_prepare_form($form_id, & if (!isset($form['#validate'])) { if (function_exists($form_id .'_validate')) { - $form['#validate'] = array($form_id .'_validate' => array()); + $form['#validate'] = array($form_id .'_validate'); } } if (!isset($form['#submit'])) { if (function_exists($form_id .'_submit')) { // We set submit here so that it can be altered. - $form['#submit'] = array($form_id .'_submit' => array()); + $form['#submit'] = array($form_id .'_submit'); } } - drupal_alter('form', $form, $form_id); - - $form = form_builder($form_id, $form); + drupal_alter('form', $form, $form_id, $storage); } @@ -366,9 +401,18 @@ function drupal_prepare_form($form_id, & * theming, and hook_form_alter functions. * @param $form * An associative array containing the structure of the form. - * + * @param $form_state + * A keyed array containing the current state of the form. This is + * used primarily by multi-step forms, and the exact contents of + * the array vary from form to form. Validation handlers can use + * this variable to store information that will later be used by + * the form's submit handlers. For example: + * $form_state['data_for_submision'] = $data; + * This technique is useful when validation requires file parsing, + * web service requests, or other expensive requests that should + * not be repeated in the submission step. */ -function drupal_validate_form($form_id, $form) { +function drupal_validate_form($form_id, $form, &$form_state) { global $form_values; static $validated_forms = array(); @@ -385,12 +429,12 @@ function drupal_validate_form($form_id, } } - if (!$form['#programmed'] && !$form['#skip_duplicate_check'] && isset($_SESSION['last_submitted']['hash']) && $_SESSION['last_submitted']['hash'] == md5(serialize($form['form_id']['#post']))) { + if (!$form['#programmed'] && !$form['#skip_duplicate_check'] && isset($_SESSION['last_submitted']['hash']) && $_SESSION['last_submitted']['hash'] == md5(serialize($form['#post']))) { // This is a repeat submission. drupal_redirect_form(NULL, $_SESSION['last_submitted']['destination']); } - _form_validate($form, $form_id); + _form_validate($form, $form_state, $form_id); $validated_forms[$form_id] = TRUE; } @@ -398,45 +442,39 @@ function drupal_validate_form($form_id, * Processes user-submitted form data from a global variable using * the submit functions defined in a structured form array. * - * @param $form_id - * A unique string identifying the form for validation, submission, - * theming, and hook_form_alter functions. * @param $form * An associative array containing the structure of the form. - * @return - * A string containing the path of the page to display when processing - * is complete. - * - */ -function drupal_submit_form($form_id, $form) { + * @param $form_state + * A keyed array containing the current state of the form. This can + * include information passed on from validation handlers, persistent + * storage from previous steps of a complex multi-page form, and + * the results generated by other submit handlers attached to the + * form. Standard elements of the $form_state include: + * $form_state['redirect']: the path to redirect to when the form + * has been processed. + * $form_state['storage']: the persistent data store that will be + * preserved between page reloads. If $form_state['storage'] is + * set, $form_state['redirect'] will be ignored and the form will + * be re-built using the stored data. + */ +function drupal_submit_form($form, &$form_state) { global $form_values; - $default_args = array($form_id, &$form_values); $submitted = FALSE; - $goto = NULL; if (isset($form['#submit'])) { - foreach ($form['#submit'] as $function => $args) { + foreach ($form['#submit'] as $function) { if (function_exists($function)) { - $args = array_merge($default_args, (array) $args); - // Since we can only redirect to one page, only the last redirect - // will work. - $redirect = call_user_func_array($function, $args); + $function($form_values, $form, $form_state); $submitted = TRUE; - if (isset($redirect)) { - $goto = $redirect; - } } } } - // Successful submit. Hash this form's POST and store the hash in the + + // Successful submit. Hash this form's POST and storage the hash in the // session. We'll use this hash later whenever this user submits another // form to make sure no identical forms get submitted twice. if ($submitted && !$form['#skip_duplicate_check']) { - $_SESSION['last_submitted'] = array('destination' => $goto, 'hash' => md5(serialize($form['form_id']['#post']))); - } - - if (isset($goto)) { - return $goto; + $_SESSION['last_submitted'] = array('destination' => $form_state['redirect'], 'hash' => md5(serialize($form['#post']))); } } @@ -513,15 +551,21 @@ function drupal_redirect_form($form, $re * * @param $elements * An associative array containing the structure of the form. + * @param $form_state + * A keyed array containing the current state of the form. This is + * used primarily by multi-step forms, and the exact contents of + * the array vary from form to form. Validation handlers can use + * this variable to store information that will later be used by + * the form's submit handlers. * @param $form_id * A unique string identifying the form for validation, submission, * theming, and hook_form_alter functions. */ -function _form_validate($elements, $form_id = NULL) { +function _form_validate($elements, &$form_state, $form_id = NULL) { // Recurse through all children. foreach (element_children($elements) as $key) { if (isset($elements[$key]) && $elements[$key]) { - _form_validate($elements[$key]); + _form_validate($elements[$key], $form_state); } } /* Validate the current input */ @@ -564,16 +608,22 @@ function _form_validate($elements, $form } } - // Call user-defined validators. - if (isset($elements['#validate'])) { - foreach ($elements['#validate'] as $function => $args) { - $args = array_merge(array($elements), $args); - // For the full form we hand over a copy of $form_values. - if (isset($form_id)) { - $args = array_merge(array($form_id, $GLOBALS['form_values']), $args); + // Call user-defined form level validators. + if (isset($form_id)) { + if (isset($elements['#validate'])) { + foreach ($elements['#validate'] as $function) { + if (function_exists($function)) { + $function($GLOBALS['form_values'], $elements, $form_state); + } } + } + } + // Call any element-specific validators. These must act on the element + // #value data, rather than the general $form_values collection. + elseif (isset($elements['#element_validate'])) { + foreach ($elements['#element_validate'] as $function) { if (function_exists($function)) { - call_user_func_array($function, $args); + $function($elements, $form_state); } } } @@ -1117,7 +1167,7 @@ function expand_password_confirm($elemen '#title' => t('Confirm password'), '#value' => $element['#value']['pass2'], ); - $element['#validate'] = array('password_confirm_validate' => array()); + $element['#element_validate'] = array('password_confirm_validate'); $element['#tree'] = TRUE; if (isset($element['#size'])) { Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.97 diff -u -p -r1.97 system.install --- modules/system/system.install 24 Apr 2007 13:55:36 -0000 1.97 +++ modules/system/system.install 25 Apr 2007 01:43:53 -0000 @@ -3798,6 +3798,42 @@ function system_update_6010() { } /** + * Add the form cache table. + */ +function system_update_6011() { + $ret = array(); + + switch ($GLOBALS['db_type']) { + case 'pgsql': + $ret[] = update_sql("CREATE TABLE {cache_form} ( + cid varchar(255) NOT NULL default '', + data bytea, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + PRIMARY KEY (cid) + )"); + $ret[] = update_sql("CREATE INDEX {cache_form}_expire_idx ON {cache_form} (expire)"); + break; + case 'mysql': + case 'mysqli': + $ret[] = update_sql("CREATE TABLE {cache_form} ( + cid varchar(255) NOT NULL default '', + data longblob, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + PRIMARY KEY (cid), + INDEX expire (expire) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + break; + } + + return $ret; +} + + +/** * @} End of "defgroup updates-5.x-to-6.x" * The next series of updates should start at 7000. */