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	24 Apr 2007 19:17:28 -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,22 @@ 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);
+  $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);
     // 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.
     if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) {
-      $redirect = drupal_submit_form($form_id, $form);
+      $form_state['redirect'] = NULL;
+      drupal_submit_form($form, $form_state);
       if (!$form['#programmed']) {
-        drupal_redirect_form($form, $redirect);
+        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');
+        }
+        drupal_redirect_form($form, $form_state['redirect']);
       }
     }
   }
@@ -264,9 +285,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 +297,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 +344,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 +358,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 +382,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 +410,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 +423,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 +532,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 +589,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 +1148,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	24 Apr 2007 19:17:32 -0000
@@ -3737,6 +3737,41 @@ function system_update_6008() {
   }
 
   // Rebuild system table contents.
+ * Add the form cache table.
+ */
+function system_update_6009() {
+  $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;
+}
+
+/**
   module_rebuild_cache();
   system_theme_data();
