### Eclipse Workspace Patch 1.0
#P test_drupal_6
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.188
diff -u -r1.188 form.inc
--- includes/form.inc	13 Apr 2007 08:56:57 -0000	1.188
+++ includes/form.inc	16 Apr 2007 13:31:54 -0000
@@ -420,7 +420,20 @@
         $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);
+        if (batch_get()) {
+          // Some previous _submit callback has set a batch :
+          // append the _submit callback to the batch
+          batch_open(array(), TRUE); // TODO : awkward - open a new batch set for the new submit callback
+          batch_add('_batch_submit_form', array($function, $args));
+        }
+        else {
+          $redirect = call_user_func_array($function, $args);
+          if (batch_get()) {
+            // The _submit callback has opened a batch :
+            // store the form info
+            _batch_set_form_info($form_id, $form, $form_values);
+          }
+        }
         $submitted = TRUE;
         if (isset($redirect)) {
           $goto = $redirect;
@@ -487,6 +500,11 @@
  *
  */
 function drupal_redirect_form($form, $redirect = NULL) {
+  if (!$form['#programmed'] && ($_batch = batch_get()) && !isset($_batch['finished'])) {
+    // Redirect to batch page
+    batch_execute();
+  }
+
   if (isset($redirect)) {
     $goto = $redirect;
   }
@@ -954,7 +972,7 @@
     }
     else {
       $key = (string)$key;
-      if ($value_valid && ((string)$element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) {
+      if ($value_valid && ((!$value_is_array && (string)$element['#value'] === $key) || ($value_is_array && in_array($key, $element['#value'])))) {
         $selected = ' selected="selected"';
       }
       else {
Index: includes/theme.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/theme.inc,v
retrieving revision 1.349
diff -u -r1.349 theme.inc
--- includes/theme.inc	15 Apr 2007 15:36:10 -0000	1.349
+++ includes/theme.inc	16 Apr 2007 13:31:55 -0000
@@ -733,7 +733,7 @@
     'logo' => base_path() .'themes/garland/minnelli/logo.png',
     'site_title' => t('Drupal'),
     'title' => drupal_get_title(),
-    'messages' => theme('status_messages'),
+    'messages' => $messages ? theme('status_messages') : '',
     'content' => $content,
   );
 
@@ -1394,9 +1394,9 @@
 
 function theme_progress_bar($percent, $message) {
   $output = '<div id="progress" class="progress">';
-  $output .= '<div class="percentage">'. $percent .'%</div>';
-  $output .= '<div class="status">'. $message .'</div>';
   $output .= '<div class="bar"><div class="filled" style="width: '. $percent .'%"></div></div>';
+  $output .= '<div class="percentage">'. $percent .'%</div>';
+  $output .= '<div class="message">'. $message .'</div>';
   $output .= '</div>';
 
   return $output;
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.629
diff -u -r1.629 common.inc
--- includes/common.inc	15 Apr 2007 15:36:10 -0000	1.629
+++ includes/common.inc	16 Apr 2007 13:31:54 -0000
@@ -1877,6 +1877,7 @@
   require_once './includes/unicode.inc';
   require_once './includes/image.inc';
   require_once './includes/form.inc';
+  require_once './includes/batch.inc';
   // Set the Drupal custom error handler.
   set_error_handler('drupal_error_handler');
   // Emit the correct charset HTTP header.
Index: misc/drupal.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.js,v
retrieving revision 1.30
diff -u -r1.30 drupal.js
--- misc/drupal.js	9 Apr 2007 13:58:02 -0000	1.30
+++ misc/drupal.js	16 Apr 2007 13:31:55 -0000
@@ -222,5 +222,8 @@
 
 // Global Killswitch on the <html> element
 if (Drupal.jsEnabled) {
+  // Global Killswitch on the <html> element
   document.documentElement.className = 'js';
+  // 'js enabled' cookie
+  document.cookie = 'has_js=1';
 }
Index: misc/progress.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/progress.js,v
retrieving revision 1.14
diff -u -r1.14 progress.js
--- misc/progress.js	14 Dec 2006 14:21:36 -0000	1.14
+++ misc/progress.js	16 Apr 2007 13:31:55 -0000
@@ -20,9 +20,9 @@
   this.element = document.createElement('div');
   this.element.id = id;
   this.element.className = 'progress';
-  $(this.element).html('<div class="percentage"></div>'+
-                       '<div class="message">&nbsp;</div>'+
-                       '<div class="bar"><div class="filled"></div></div>');
+  $(this.element).html('<div class="bar"><div class="filled"></div></div>'+
+                       '<div class="percentage"></div>'+
+                       '<div class="message">&nbsp;</div>');
 }
 
 /**
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.467
diff -u -r1.467 system.module
--- modules/system/system.module	13 Apr 2007 08:56:59 -0000	1.467
+++ modules/system/system.module	16 Apr 2007 13:31:55 -0000
@@ -327,6 +327,12 @@
     'page callback' => 'system_sql',
     'type' => MENU_CALLBACK,
   );
+  // Batch operations
+  $items['batch'] = array(
+    'page callback' => 'system_batch_page',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -2446,5 +2452,20 @@
 function system_cron() {
   // Cleanup the flood
   db_query('DELETE FROM {flood} WHERE timestamp < %d', time() - 3600);
+  // Cleanup the batch table
+  db_query('DELETE FROM {batch} WHERE timestamp < %d', time()-864000);
 }
 
+/**
+ * Default page callback for batches.
+ */
+function system_batch_page() {
+  $output = _batch_page();
+  if ($output === FALSE) {
+    drupal_access_denied();
+  }
+  else {
+    // Force a page without blocks
+    print theme('page', $output, FALSE);
+  }
+}
Index: includes/batch.inc
===================================================================
RCS file: includes/batch.inc
diff -N includes/batch.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/batch.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,472 @@
+<?php
+
+/**
+ * @file Batch processing API for processes to run in multiple HTTP requests
+ */
+
+/**
+ * API :
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   $batch = array(
+ *     'title' => t('batch_title'),
+ *     'init_message' => t('batch_init_message'),
+ *     'error_message' => t('batch_message_error'),
+ *     'finished' => 'my_finished_callback',
+ *     'operations' => array(
+ *        array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar')),
+ *        array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2')),
+ *       ...
+ *     );
+ *   );
+ *   batch_open($batch);
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ * OR (short form) :
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   batch_open();
+ *   batch_add('my_func_1', array($arg1));
+ *   batch_add('my_func_2', array($arg1, $arg2, $arg3));
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ */
+
+/**
+ * Open a new batch (or a new batch set if a batch has already
+ * been opened in the current execution)
+ *
+ * @param $batch
+ *   An array decribing the batch.
+ *   Example :
+ *   @code
+ *   $batch = array(
+ *     'title' => t('Descriptive batch title'),
+ *     'init_message' => t('Initial message'),
+ *     // @current, @remaining, @total and @percent can be used in progress_message
+ *     'progress_message' => t('Remaining @remaining of @total.'),
+ *     'error_message' => t('Error message'),
+ *     'finished' => 'my_finished_callback',
+ *     'operations' => array(
+ *        array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar'), 'task' => 'performing task 1'),
+ *        array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2'), 'task => 'performing task 2'),
+ *        ...
+ *     );
+ *   );
+ *   @endcode
+ *
+ */
+function batch_open($batch, $new_set = FALSE, $url = 'batch', $redirect = '') {
+  global $_batch;
+
+  if (!isset($_batch)) {
+    // new batch
+    $_batch = array(
+      'id' => db_next_id('{batch}_bid'),
+      'running' => FALSE,
+      'current_set' => 0,
+      'url' => $url,
+      'redirect' => $redirect,
+      'source_page' => $_GET['q'],
+    );
+  }
+  elseif ($new_set) {
+    // new batch set
+    $_batch['current_set']++;
+  }
+  $init = array(
+    'total' => 0,
+    'operations' => array(),
+    'operations_pending' => array(),
+    'sandbox' => array(),
+    'results' => array(),
+    'success' => FALSE,
+  );
+  $defaults = array(
+    'title' => t('Processing'),
+    'init_message' => t('Initializing...'),
+    'error_message' => t('An error has occurred.'),
+  );
+
+  // TODO : currently, only the first set sets the title of the page (which does not get reloaded)
+  $_batch['sets'][$_batch['current_set']] = $init + (array) $batch + $defaults;
+  $error_link = '<br/>'. t('Please continue to !link', array('!link' => '<a href="'. url($_batch['url'], array('query' => array('id' => $_batch['id'], 'op' => 'error'))) .'">the summary page</a>'));
+  $_batch['sets'][$_batch['current_set']]['error_message'] .= $error_link;
+
+  if (isset($batch['operations'])) {
+    batch_add_operations($batch['operations']);
+  }
+}
+
+/**
+ * Add a single operation to the current batch.
+ * Takes its argument in the in the same form as call_user_func_array.
+ *
+ * @param $function
+ *   The name of the function.
+ * @param $args
+ *   The array of arguments.
+ */
+function batch_add($function, $args = array()) {
+  $op = array('callback' => $function, 'arguments' => $args);
+  batch_add_operations(array($op));
+}
+
+/**
+ * Add an array of operations to the current batch.
+ *
+ * @param $operations
+ *   The name of the function.
+ */
+function batch_add_operations($operations) {
+  global $_batch;
+  if (isset($_batch)) {
+    $curr_set = &$_batch['sets'][$_batch['current_set']];
+    // TODO : the whole "if running / operations_pending" stuff could probably be thrown away
+    // by dealing with submit callbacks differently
+    if ($_batch['running']) {
+      // If the batch is running, add for execution right after the currently running
+      // operation is finished.
+      // In order to ensure the correct execution order, the operations are stored in a
+      // separate queue, and inserted when the currently running op is over
+      $curr_set['operations_pending'] = array_merge($curr_set['operations_pending'], $operations);
+    }
+    else {
+      // If the batch is not running, append to the op queue
+      $curr_set['operations'] = array_merge($curr_set['operations'], $operations);
+      $curr_set['total'] += count($operations);
+    }
+  }
+}
+
+function _batch_operations_insert_pending() {
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+  if (isset($curr_set['operations_pending'])) {
+    $curr_set['operations'] = array_merge($curr_set['operations_pending'], $curr_set['operations']);
+    $curr_set['total'] += count($curr_set['operations_pending']);
+    $curr_set['operations_pending'] = array();
+  }
+}
+
+function _batch_set_form_info($form_id, $form, $form_values) {
+  global $_batch;
+  if (isset($_batch)) {
+    $_batch['form_id'] = $form_id;
+    $_batch['form'] = $form;
+    $_batch['form_values'] = $form_values;
+//    $_batch['_POST'] = $_POST;
+  }
+}
+
+// TODO : could be removed by using direct references to global $_batch in form.inc ?
+function batch_get() {
+  global $_batch;
+  if (isset($_batch)) {
+    return $_batch;
+  }
+}
+
+function batch_execute() {
+  global $_batch;
+  if (isset($_batch)) {
+    // Save and unset the destination if any
+    if (isset($_REQUEST['destination'])) {
+      $_batch['destination'] = $_REQUEST['destination'];
+      unset($_REQUEST['destination']);
+    }
+    elseif (isset($_REQUEST['edit']['destination'])) {
+      $_batch['destination'] = $_REQUEST['edit']['destination'];
+      unset($_REQUEST['edit']['destination']);
+    }
+    db_query("INSERT INTO {batch} (bid, timestamp, batch) VALUES (%d, %d, '%s')", $_batch['id'], time(), serialize($_batch));
+    drupal_goto($_batch['url'], 'op=start&id='.$_batch['id']);
+  }
+}
+
+// Encapsulate a batched call to a form _submit callback,
+// storing the redirection and the new value of $form_values.
+function _batch_submit_form($function, $args) {
+  global $_batch;
+  $args[1] = &$_batch['form_values'];
+  $redirect = call_user_func_array($function, $args);
+  if (isset($redirect)) {
+    $_batch['redirect'] = $redirect;
+  }
+}
+
+/**
+ * State based dispatcher for batches.
+ */
+function _batch_page() {
+  global $_batch;
+
+  if (isset($_REQUEST['id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $_REQUEST['id']))) {
+    $_batch = unserialize($data);
+  }
+  else {
+    return FALSE;
+  }
+
+  // Register database update for end of processing
+  register_shutdown_function('_batch_shutdown');
+
+  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  switch ($op) {
+    case 'finished':
+      // Finish with success.
+      $output = _batch_finished(TRUE);
+      break;
+
+    case 'error':
+      // Finish with error.
+      // TODO : is this still needed ?
+      $output = _batch_finished(FALSE);
+      break;
+
+    case 'do':
+      $output = _batch_do();
+      break;
+
+    case 'do_nojs':
+      $output = _batch_progress_page_nojs();
+      break;
+
+    case 'start':
+      $output = _batch_start();
+      break;
+  }
+
+  return $output;
+}
+
+/**
+ * Perform initial preparation for running a batch, choose between the
+ * JS and non-JS version.
+ */
+function _batch_start() {
+  global $_batch;
+
+  $_batch['current_set'] = 0;
+//  if ($_batch['total'] == 0) {
+//    return _batch_finished(TRUE); // TODO : error or no error ?
+//  }
+
+  // drupal.js remembers js enabled users via the has_js cookie.
+  // NOTE : nojs if the user did not visit any js enabled page during his browser session ?
+  if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
+    return _batch_progress_page_js();
+  }
+  else {
+    return _batch_progress_page_nojs();
+  }
+}
+
+/**
+ * Batch processing page with JavaScript support.
+ */
+function _batch_progress_page_js() {
+  // TODO : ideally, the js progress would be in a popup, so that the page
+  // does not appear in the browser's history (plus that would be definitely neat !)
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+
+  $_batch['running'] = TRUE;
+  drupal_set_title($curr_set['title']);
+  // Prevent browser from using cached drupal.js or update.js
+  drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE);
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'])));
+  $js_setting = array(
+    'batch' => array(
+      'errorMessage' => $curr_set['error_message'],
+      'initMessage' => $curr_set['init_message'],
+      'uri' => $url,
+  ),
+  );
+  drupal_add_js($js_setting, 'setting');
+  drupal_add_js('misc/batch.js', 'core', 'header', FALSE, TRUE);
+
+  $output = '<div id="progress"></div>';
+  return $output;
+}
+
+/**
+ * Do one pass of execution and inform back the browser about progression
+ */
+function _batch_do() {
+  // HTTP Post required
+  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+    drupal_set_message(t('HTTP Post is required.'), 'error');
+    drupal_set_title(t('Error'));
+    return '';
+  }
+
+  list($percentage, $message) = _batch_process();
+
+  drupal_set_header('Content-Type: text/plain; charset=utf-8');
+  print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+  exit();
+}
+
+/**
+ * Batch processing page without JavaScript support.
+ */
+function _batch_progress_page_nojs() {
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+
+  drupal_set_title($curr_set['title']);
+
+  $new_op = 'do_nojs';
+
+  if (!$_batch['running']) {
+    // This is the first page so return some output immediately.
+    $percentage = 0;
+    $message = $curr_set['init_message'];
+    $_batch['running'] = TRUE;
+  }
+  else {
+    // This is one of the later requests: do some updates first.
+
+    // Error handling: if PHP dies due to a fatal error (e.g. non-existant
+    // function), it will output whatever is in the output buffer,
+    // followed by the error message.
+    ob_start();
+    $fallback = $curr_set['error_message_no_js'];
+    $fallback = theme('maintenance_page', $fallback, FALSE);
+
+    // We strip the end of the page using a marker in the template, so any
+    // additional HTML output by PHP shows up inside the page rather than
+    // below it. While this causes invalid HTML, the same would be true if
+    // we didn't, as content is not allowed to appear after </html> anyway.
+    list($fallback) = explode('<!--partial-->', $fallback);
+    print $fallback;
+
+    list($percentage, $message) = _batch_process($_batch);
+    if ($percentage == 100) {
+      $new_op = 'finished';
+    }
+
+    // Updates successful; remove fallback
+    ob_end_clean();
+  }
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'], 'op' => $new_op)));
+  drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL='. $url .'">');
+  $output = theme('progress_bar', $percentage, $message);
+  return $output;
+
+  // TODO : update.php has the following lines :
+  // Note: do not output drupal_set_message()s until the summary page.
+  //print theme('maintenance_page', $output, FALSE);
+  //return NULL;
+}
+
+/**
+ * Process the batch for 1 second
+ */
+function _batch_process() {
+  global $_batch;
+
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+  // TODO : while (list($key, $op) = each($curr_set['operations'])) {
+  while (isset($curr_set['operations']) && $op = reset($curr_set['operations'])) {
+    $finished = 1;
+    if (is_array($op) && isset($op['callback']) && function_exists($op['callback'])) {
+      $batch_args = array('sandbox' => &$curr_set['sandbox'], 'results' => &$curr_set['results'], 'finished' => &$finished, 'message' => '');
+      call_user_func_array($op['callback'], array_merge($op['arguments'], array(&$batch_args)));
+      $task_message = $batch_args['message'];
+    }
+    if ($finished == 1) {
+      // Remove the operation, and clear the sandbox to reduce the stored data.
+      array_shift($curr_set['operations']);
+      $curr_set['sandbox'] = array();
+
+      // Add the operations that were added during the execution
+      _batch_operations_insert_pending();
+
+      // If the current set is finished, continue with the next one.
+      if (empty($curr_set['operations'])) {
+        $curr_set['success'] = TRUE;
+        if (isset($_batch['sets'][++$_batch['current_set']])) {
+          $curr_set = &$_batch['sets'][$_batch['current_set']];
+        }
+      }
+      // Make sure this step isn't counted double
+      $finished = 0;
+    }
+    if (timer_read('page') > 1000) {
+      break;
+    }
+  }
+
+  $remaining  = count($curr_set['operations']);
+  $total      = $curr_set['total'];
+  $current    = $total - $remaining + $finished;
+  $percentage = floor($current / $total * 100);
+  $values = array(
+    '@remaining'  => $remaining,
+    '@total'      => $total,
+    '@current'    => floor($current),
+    '@percentage' => $percentage,
+    );
+  $progress_message = strtr('Remaining @remaining of @total.', $values);
+
+  $message = $progress_message .'<br/>';
+  $message.= $task_message ? $task_message : '&nbsp';
+
+  return array($percentage, $message);
+}
+
+/**
+ * End the batch : allow for results massaging and redirect to the right page
+ */
+function _batch_finished() {
+  global $_batch;
+
+  // TODO : one of those is probably redundant
+  $_batch['running'] = FALSE;
+  $_batch['finished'] = TRUE;
+
+  // Call the 'finished' callbacks
+  foreach($_batch['sets'] as $key => $batch_set) {
+    if (isset($batch_set['finished']) && function_exists($batch_set['finished'])) {
+      $batch_set['finished']($batch_set['success'], $batch_set['results']);
+    }
+  }
+
+  // Redirect
+  if (isset($_batch['destination'])) {
+    $_REQUEST['destination'] = $_batch['destination'];
+  }
+
+  $form = isset($_batch['form']) ? $_batch['form'] : NULL;
+  $redirect = isset($_batch['redirect']) ? $_batch['redirect'] : $_batch['source_page'];
+  drupal_redirect_form($form, $redirect);
+
+  // no redirection : we had a redirection to FALSE : multistep form ?
+  // TODO : not working right now - modify form.inc to account for this case
+//  $_POST = $_batch['_POST'];
+//  $args = $_SESSION['form'][$form['#build_id']]['args'];
+//  $form = call_user_func_array('drupal_retrieve_form', $args);
+//  return drupal_get_form($_batch['form_id']);
+  drupal_goto($_batch['source_page']);
+}
+
+/**
+ * Store tha batch data for next request, or clear the table if the batch is finished
+ */
+function _batch_shutdown() {
+  global $_batch;
+  if ($_batch['running']) {
+    db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($_batch), $_batch['id']);
+  }
+  else {
+    db_query("DELETE FROM {batch} WHERE bid = %d", $_batch['id']);
+  }
+}
Index: includes/batch_prev.inc
===================================================================
RCS file: includes/batch_prev.inc
diff -N includes/batch_prev.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/batch_prev.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,471 @@
+<?php
+
+/**
+ * @file Batch processing API for processes to run in multiple HTTP requests
+ */
+
+/**
+ * API :
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   $batch = array(
+ *     'title' => t('batch_title'),
+ *     'init_message' => t('batch_init_message'),
+ *     'error_message' => t('batch_message_error'),
+ *     'finished' => 'my_finished_callback',
+ *     'operations' => array(
+ *        array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar')),
+ *        array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2')),
+ *       ...
+ *     );
+ *   );
+ *   batch_open($batch);
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ * OR (short form) :
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   batch_open();
+ *   batch_add('my_func_1', array($arg1));
+ *   batch_add('my_func_2', array($arg1, $arg2, $arg3));
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ */
+
+/**
+ * Open a new batch (or a new batch set if a batch has already
+ * been opened in the current execution)
+ *
+ * @param $batch
+ *   An array decribing the batch.
+ *   Example :
+ *   @code
+ *   $batch = array(
+ *     'title' => t('Descriptive batch title'),
+ *     'init_message' => t('Initial message'),
+ *     // @current, @remaining, @total and @percent can be used in progress_message
+ *     'progress_message' => t('Remaining @remaining of @total.'),
+ *     'error_message' => t('Error message'),
+ *     'finished' => 'my_finished_callback',
+ *     'operations' => array(
+ *        array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar'), 'task' => 'performing task 1'),
+ *        array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2'), 'task => 'performing task 2'),
+ *        ...
+ *     );
+ *   );
+ *   @endcode
+ *
+ */
+function batch_open($batch, $new_set = FALSE, $url = 'batch') {
+  global $_batch;
+
+  if (!isset($_batch)) {
+    // new batch
+    $_batch = array(
+      'id' => db_next_id('{batch}_bid'),
+      'running' => FALSE,
+      'current_set' => 0,
+      'url' => $url,
+      'source_page' => $_GET['q'],
+    );
+  }
+  elseif ($new_set) {
+    // new batch set
+    $_batch['current_set']++;
+  }
+  $init = array(
+    'total' => 0,
+    'operations' => array(),
+    'operations_pending' => array(),
+    'sandbox' => array(),
+    'results' => array(),
+    'success' => FALSE,
+  );
+  $defaults = array(
+    'title' => t('Processing'),
+    'init_message' => t('Initializing...'),
+    'error_message' => t('An error has occurred.'),
+  );
+
+  // TODO : currently, only the first set sets the title of the page (which does not get reloaded)
+  $_batch['sets'][$_batch['current_set']] = $init + (array) $batch + $defaults;
+  $error_link = '<br/>'. t('Please continue to !link', array('!link' => '<a href="'. url($_batch['url'], array('query' => array('id' => $_batch['id'], 'op' => 'error'))) .'">the error page</a>'));
+  $_batch['sets'][$_batch['current_set']]['error_message'] .= $error_link;
+
+  if (isset($batch['operations'])) {
+    batch_add_operations($batch['operations']);
+  }
+}
+
+/**
+ * Add a single operation to the current batch.
+ * Takes its argument in the in the same form as call_user_func_array.
+ *
+ * @param $function
+ *   The name of the function.
+ * @param $args
+ *   The array of arguments.
+ */
+function batch_add($function, $args = array()) {
+  $op = array('callback' => $function, 'arguments' => $args);
+  batch_add_operations(array($op));
+}
+
+/**
+ * Add an array of operations to the current batch.
+ *
+ * @param $operations
+ *   The name of the function.
+ */
+function batch_add_operations($operations) {
+  global $_batch;
+  if (isset($_batch)) {
+    $curr_set = &$_batch['sets'][$_batch['current_set']];
+    // TODO : the whole "if running / operations_pending" stuff could probably be thrown away
+    // by dealing with submit callbacks differently
+    if ($_batch['running']) {
+      // If the batch is running, add for execution right after the currently running
+      // operation is finished.
+      // In order to ensure the correct execution order, the operations are stored in a
+      // separate queue, and inserted when the currently running op is over
+      $curr_set['operations_pending'] = array_merge($curr_set['operations_pending'], $operations);
+    }
+    else {
+      // If the batch is not running, append to the op queue
+      $curr_set['operations'] = array_merge($curr_set['operations'], $operations);
+      $curr_set['total'] += count($operations);
+    }
+  }
+}
+
+function _batch_operations_insert_pending() {
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+  if (isset($curr_set['operations_pending'])) {
+    $curr_set['operations'] = array_merge($curr_set['operations_pending'], $curr_set['operations']);
+    $curr_set['total'] += count($curr_set['operations_pending']);
+    $curr_set['operations_pending'] = array();
+  }
+}
+
+function _batch_set_form_info($form_id, $form, $form_values) {
+  global $_batch;
+  if (isset($_batch)) {
+    $_batch['form_id'] = $form_id;
+    $_batch['form'] = $form;
+    $_batch['form_values'] = $form_values;
+//    $_batch['_POST'] = $_POST;
+  }
+}
+
+// TODO : could be removed by using direct references to global $_batch in form.inc ?
+function batch_get() {
+  global $_batch;
+  if (isset($_batch)) {
+    return $_batch;
+  }
+}
+
+function batch_execute() {
+  global $_batch;
+  if (isset($_batch)) {
+    // Save and unset the destination if any
+    if (isset($_REQUEST['destination'])) {
+      $_batch['destination'] = $_REQUEST['destination'];
+      unset($_REQUEST['destination']);
+    }
+    elseif (isset($_REQUEST['edit']['destination'])) {
+      $_batch['destination'] = $_REQUEST['edit']['destination'];
+      unset($_REQUEST['edit']['destination']);
+    }
+    db_query("INSERT INTO {batch} (bid, timestamp, batch) VALUES (%d, %d, '%s')", $_batch['id'], time(), serialize($_batch));
+    drupal_goto($_batch['url'], 'op=start&id='.$_batch['id']);
+  }
+}
+
+// Encapsulate a batched call to a form _submit callback,
+// storing the redirection and the new value of $form_values.
+function _batch_submit_form($function, $args) {
+  global $_batch;
+  $args[1] = &$_batch['form_values'];
+  $redirect = call_user_func_array($function, $args);
+  if (isset($redirect)) {
+    $_batch['redirect'] = $redirect;
+  }
+}
+
+/**
+ * State based dispatcher for batches.
+ */
+function _batch_page() {
+  global $_batch;
+
+  if (isset($_REQUEST['id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $_REQUEST['id']))) {
+    $_batch = unserialize($data);
+  }
+  else {
+    return FALSE;
+  }
+
+  // Register database update for end of processing
+  register_shutdown_function('_batch_shutdown');
+
+  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  switch ($op) {
+    case 'finished':
+      // Finish with success.
+      $output = _batch_finished(TRUE);
+      break;
+
+    case 'error':
+      // Finish with error.
+      // TODO : is this still needed ?
+      $output = _batch_finished(FALSE);
+      break;
+
+    case 'do':
+      $output = _batch_do();
+      break;
+
+    case 'do_nojs':
+      $output = _batch_progress_page_nojs();
+      break;
+
+    case 'start':
+      $output = _batch_start();
+      break;
+  }
+
+  return $output;
+}
+
+/**
+ * Perform initial preparation for running a batch, choose between the
+ * JS and non-JS version.
+ */
+function _batch_start() {
+  global $_batch;
+
+  $_batch['current_set'] = 0;
+//  if ($_batch['total'] == 0) {
+//    return _batch_finished(TRUE); // TODO : error or no error ?
+//  }
+
+  // drupal.js remembers js enabled users via the has_js cookie.
+  // NOTE : nojs if the user did not visit any js enabled page during his browser session ?
+  if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
+    return _batch_progress_page_js();
+  }
+  else {
+    return _batch_progress_page_nojs();
+  }
+}
+
+/**
+ * Batch processing page with JavaScript support.
+ */
+function _batch_progress_page_js() {
+  // TODO : ideally, the js progress would be in a popup, so that the page
+  // does not appear in the browser's history (plus that would be definitely neat !)
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+
+  $_batch['running'] = TRUE;
+  drupal_set_title($curr_set['title']);
+  // Prevent browser from using cached drupal.js or update.js
+  drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE);
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'])));
+  $js_setting = array(
+    'batch' => array(
+      'errorMessage' => $curr_set['error_message'],
+      'initMessage' => $curr_set['init_message'],
+      'uri' => $url,
+  ),
+  );
+  drupal_add_js($js_setting, 'setting');
+  drupal_add_js('misc/batch.js', 'core', 'header', FALSE, TRUE);
+
+  $output = '<div id="progress"></div>';
+  return $output;
+}
+
+/**
+ * Do one pass of execution and inform back the browser about progression
+ */
+function _batch_do() {
+  // HTTP Post required
+  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+    drupal_set_message(t('HTTP Post is required.'), 'error');
+    drupal_set_title(t('Error'));
+    return '';
+  }
+
+  list($percentage, $message) = _batch_process();
+
+  drupal_set_header('Content-Type: text/plain; charset=utf-8');
+  print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+  exit();
+}
+
+/**
+ * Batch processing page without JavaScript support.
+ */
+function _batch_progress_page_nojs() {
+  global $_batch;
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+
+  drupal_set_title($curr_set['title']);
+
+  $new_op = 'do_nojs';
+
+  if (!$_batch['running']) {
+    // This is the first page so return some output immediately.
+    $percentage = 0;
+    $message = $curr_set['init_message'];
+    $_batch['running'] = TRUE;
+  }
+  else {
+    // This is one of the later requests: do some updates first.
+
+    // Error handling: if PHP dies due to a fatal error (e.g. non-existant
+    // function), it will output whatever is in the output buffer,
+    // followed by the error message.
+    ob_start();
+    $fallback = $curr_set['error_message_no_js'];
+    $fallback = theme('maintenance_page', $fallback, FALSE);
+
+    // We strip the end of the page using a marker in the template, so any
+    // additional HTML output by PHP shows up inside the page rather than
+    // below it. While this causes invalid HTML, the same would be true if
+    // we didn't, as content is not allowed to appear after </html> anyway.
+    list($fallback) = explode('<!--partial-->', $fallback);
+    print $fallback;
+
+    list($percentage, $message) = _batch_process($_batch);
+    if ($percentage == 100) {
+      $new_op = 'finished';
+    }
+
+    // Updates successful; remove fallback
+    ob_end_clean();
+  }
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'], 'op' => $new_op)));
+  drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL='. $url .'">');
+  $output = theme('progress_bar', $percentage, $message);
+  return $output;
+
+  // TODO : update.php has the following lines :
+  // Note: do not output drupal_set_message()s until the summary page.
+  //print theme('maintenance_page', $output, FALSE);
+  //return NULL;
+}
+
+/**
+ * Process the batch for 1 second
+ */
+function _batch_process() {
+  global $_batch;
+
+  $curr_set = &$_batch['sets'][$_batch['current_set']];
+  // TODO : while (list($key, $op) = each($curr_set['operations'])) {
+  while (isset($curr_set['operations']) && $op = reset($curr_set['operations'])) {
+    $finished = 1;
+    if (is_array($op) && isset($op['callback']) && function_exists($op['callback'])) {
+      $batch_args = array('sandbox' => &$curr_set['sandbox'], 'results' => &$curr_set['results'], 'finished' => $finished, 'message' => '');
+      call_user_func_array($op['callback'], array_merge($op['arguments'], array(&$batch_args)));
+      $task_message = $batch_args['message'];
+    }
+    if ($finished == 1) {
+      // Remove the operation, and clear the sandbox to reduce the stored data.
+      array_shift($curr_set['operations']);
+      $curr_set['sandbox'] = array();
+
+      // Add the operations that were added during the execution
+      _batch_operations_insert_pending();
+
+      // If the current set is finished, continue with the next one.
+      if (empty($curr_set['operations'])) {
+        $curr_set['success'] = TRUE;
+        if (isset($_batch['sets'][++$_batch['current_set']])) {
+          $curr_set = &$_batch['sets'][$_batch['current_set']];
+        }
+      }
+      // Make sure this step isn't counted double
+      $finished = 0;
+    }
+    if (timer_read('page') > 1000) {
+      break;
+    }
+  }
+
+  $remaining  = count($curr_set['operations']);
+  $total      = $curr_set['total'];
+  $current    = $total - $remaining + $finished;
+  $percentage = floor($current / $total * 100);
+  $values = array(
+    '@remaining'  => $remaining,
+    '@total'      => $total,
+    '@current'    => floor($current),
+    '@percentage' => $percentage,
+    );
+  $progress_message = strtr('Remaining @remaining of @total.', $values);
+
+  $message = $progress_message .'<br/>';
+  $message.= $task_message ? $task_message : '&nbsp';
+
+  return array($percentage, $message);
+}
+
+/**
+ * End the batch : allow for results massaging and redirect to the right page
+ */
+function _batch_finished() {
+  global $_batch;
+
+  // TODO : one of those is probably redundant
+  $_batch['running'] = FALSE;
+  $_batch['finished'] = TRUE;
+
+  // Call the 'finished' callbacks
+  foreach($_batch['sets'] as $key => $batch_set) {
+    if (isset($batch_set['finished']) && function_exists($batch_set['finished'])) {
+      $batch_set['finished']($batch_set['success'], $batch_set['results']);
+    }
+  }
+
+  // Redirect
+  if (isset($_batch['destination'])) {
+    $_REQUEST['destination'] = $_batch['destination'];
+  }
+
+  $form = isset($_batch['form']) ? $_batch['form'] : NULL;
+  $redirect = isset($_batch['redirect']) ? $_batch['redirect'] : $_batch['source_page'];
+  drupal_redirect_form($form, $redirect);
+
+  // no redirection : we had a redirection to FALSE : multistep form ?
+  // TODO : not working right now - modify form.inc to account for this case
+//  $_POST = $_batch['_POST'];
+//  $args = $_SESSION['form'][$form['#build_id']]['args'];
+//  $form = call_user_func_array('drupal_retrieve_form', $args);
+//  return drupal_get_form($_batch['form_id']);
+  drupal_goto($_batch['source_page']);
+}
+
+/**
+ * Store tha batch data for next request, or clear the table if the batch is finished
+ */
+function _batch_shutdown() {
+  global $_batch;
+  if ($_batch['running']) {
+    db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($_batch), $_batch['id']);
+  }
+  else {
+    db_query("DELETE FROM {batch} WHERE bid = %d", $_batch['id']);
+  }
+}
Index: includes/batch_prev_prev.inc
===================================================================
RCS file: includes/batch_prev_prev.inc
diff -N includes/batch_prev_prev.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/batch_prev_prev.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,337 @@
+<?php
+
+/**
+ * API :
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   $batch = array(
+ *     'title' => t('batch_title'),
+ *     'init_message' => t('batch_init_message'),
+ *     'error_message' => t('batch_message_error'),
+ *     'finished_callback' => 'my_finished_callback',
+ *     'operations' => array(
+ *        array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar'), 'task' => 'performing task 1'),
+ *        array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2'), 'task => 'performing task 2'),
+ *       ...
+ *     );
+ *   );
+ *   drupal_set_batch($batch);
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ * OR (short form)
+ *
+ * function my_form_submit($form_id, $form_values) {
+ *   $batch = array(
+ *     'title' => t('batch_title'),
+ *   );
+ *   drupal_set_batch($batch);
+ *   drupal_batch('my_func_1', array($arg1));
+ *   drupal_batch('my_func_2', array($arg1, $arg2, $arg3), 'performing my_func_2 with '. $arg1);
+ *
+ *   return 'whatever/redirect/path';
+ * }
+ *
+ */
+
+function drupal_set_batch($batch) {
+  global $_batch;
+
+  if (!isset($_batch)) {
+    // new batch
+    $init = array(
+      'id' => db_next_id('{batch}_bid'),
+      'running' => FALSE,
+      'total' => 0,
+      'operations' => array(),
+      'operations_pending' => array(),
+      'results' => array(),
+    );
+    $defaults = array(
+      'title' => t('Processing'),
+      'init_message' => t('Initializing...'),
+      'url' => 'batch',
+    );
+    $_batch = $init + (array)$batch + $defaults;
+  }
+  // TODO : we should allow several 'finished' callbacks
+  if (isset($batch['operations'])) {
+    drupal_batch_operations($batch['operations']);
+  }
+}
+
+function drupal_batch($function, $args, $task = NULL) {
+  $op = array('callback' => $function, 'arguments' => $args);
+  if (isset($task)) {
+    $op['task'] = $task;
+  }
+  drupal_batch_operations(array($op));
+}
+
+function drupal_batch_operations($operations) {
+  global $_batch;
+  if (isset($_batch)) {
+    if ($_batch['running']) {
+      // If the batch is running, add for execution right after the currently running
+      // operation is finished.
+      // In order to ensure the correct execution order, the operations are stored in a
+      // separate queue, and inserted when the currently running op is over
+      $_batch['operations_pending'] = array_merge($_batch['operations_pending'], $operations);
+    }
+    else {
+      // If the batch is not running, append to the op queue
+      $_batch['operations'] = array_merge($_batch['operations'], $operations);
+      $_batch['total'] += count($operations);
+    }
+  }
+}
+
+function _batch_operations_insert_pending() {
+  global $_batch;
+  if (isset($_batch['operations_pending'])) {
+    $_batch['operations'] = array_merge($_batch['operations_pending'], $_batch['operations']);
+    $_batch['total'] += count($_batch['operations_pending']);
+    $_batch['operations_pending'] = array();
+  }
+}
+
+function drupal_get_batch() {
+  global $_batch;
+  if (isset($_batch)) {
+    return $_batch;
+  }
+}
+
+function batch_execute() {
+  global $_batch;
+  if (isset($_batch)) {
+    // Save and unset the destination if any
+    if (isset($_REQUEST['destination'])) {
+      $_batch['destination'] = $_REQUEST['destination'];
+      unset($_REQUEST['destination']);
+    }
+    elseif (isset($_REQUEST['edit']['destination'])) {
+      $_batch['destination'] = $_REQUEST['edit']['destination'];
+      unset($_REQUEST['edit']['destination']);
+    }
+    // TODO : $form['#redirect'] is not considered
+    db_query("INSERT INTO {batch} (bid, timestamp, batch) VALUES (%d, %d, '%s')", $_batch['id'], time(), serialize($_batch));
+    drupal_goto($_batch['url'], 'op=start&id='.$_batch['id']);
+  }
+}
+
+function _batch_page() {
+  global $_batch;
+
+  if (isset($_REQUEST['id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $_REQUEST['id']))) {
+    $_batch = unserialize($data);
+  }
+  else {
+    return FALSE;
+  }
+
+  register_shutdown_function('batch_shutdown');
+
+  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  switch ($op) {
+    case 'finished':
+      $output = _batch_finished(TRUE);
+      break;
+
+    case 'error':
+      // TODO : this is update.php specific (link in the error message)
+      $output = _batch_finished(FALSE);
+      break;
+
+    case 'do':
+      $output = _batch_do();
+      break;
+
+    case 'do_nojs':
+      $output = _batch_progress_page_nojs();
+      break;
+
+    case 'start':
+      $output = _batch_start();
+      break;
+  }
+
+  return $output;
+}
+
+function _batch_start() {
+  global $_batch;
+
+  if ($_batch['total'] == 0) {
+    return _batch_finished(TRUE); // TODO : error or no error ?
+  }
+
+  // NOTE : nojs if the user did not visit any js enabled page during his browser session ?
+  if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
+    return _batch_progress_page_js();
+  }
+  else {
+    return _batch_progress_page_nojs();
+  }
+}
+
+function _batch_progress_page_js() {
+  // TODO : this should be in a popup, so that the page does not appear in the browser's history
+  global $_batch;
+
+  $_batch['running'] = TRUE;
+  drupal_set_title($_batch['title']);
+  // Prevent browser from using cached drupal.js or update.js
+  drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE);
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'])));
+  $js_setting = array(
+    'batch' => array(
+      'errorMessage' => $_batch['error_message'],
+      'initMessage' => $_batch['init_message'],
+      'uri' => $url,
+  ),
+  );
+  drupal_add_js($js_setting, 'setting');
+  drupal_add_js('misc/batch.js', 'core', 'header', FALSE, TRUE);
+
+  $output = '<div id="progress"></div>';
+  return $output;
+}
+
+function _batch_do() {
+  // HTTP Post required
+  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+    drupal_set_message('HTTP Post is required.', 'error');
+    drupal_set_title('Error');
+    return '';
+  }
+
+  list($percentage, $message) = _batch_process();
+
+  drupal_set_header('Content-Type: text/plain; charset=utf-8');
+  print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+  exit();
+}
+
+function _batch_progress_page_nojs() {
+  global $_batch;
+
+  drupal_set_title($_batch['title']);
+
+  $new_op = 'do_nojs';
+
+  if (!$_batch['running']) {
+    // This is the first page so return some output immediately.
+    $percentage = 0;
+    $message = $_batch['init_message'];
+    $_batch['running'] = TRUE;
+  }
+  else {
+    // This is one of the later requests: do some updates first.
+
+    // Error handling: if PHP dies due to a fatal error (e.g. non-existant
+    // function), it will output whatever is in the output buffer,
+    // followed by the error message.
+    ob_start();
+    $fallback = $_batch['error_message_no_js'];
+    $fallback = theme('maintenance_page', $fallback, FALSE);
+
+    // We strip the end of the page using a marker in the template, so any
+    // additional HTML output by PHP shows up inside the page rather than
+    // below it. While this causes invalid HTML, the same would be true if
+    // we didn't, as content is not allowed to appear after </html> anyway.
+    list($fallback) = explode('<!--partial-->', $fallback);
+    print $fallback;
+
+    list($percentage, $message) = _batch_process($_batch);
+    if ($percentage == 100) {
+      $new_op = 'finished';
+    }
+
+    // Updates successful; remove fallback
+    ob_end_clean();
+  }
+
+  $url = url($_batch['url'], array('query' => array('id' => $_batch['id'], 'op' => $new_op)));
+  drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL='. $url .'">');
+  $output = theme('progress_bar', $percentage, $message);
+  return $output;
+
+  // TODO : update.php has the following lines :
+  // Note: do not output drupal_set_message()s until the summary page.
+  //print theme('maintenance_page', $output, FALSE);
+  //return NULL;
+}
+
+function _batch_process() {
+  global $_batch;
+  while (isset($_batch['operations']) && $op = reset($_batch['operations'])) {
+    $op_finished = 1;
+    $task_message = isset($op['task']) ? $op['task'] : '';
+    $op['sandbox'] = isset($op['sandbox']) ? $op['sandbox'] : array();
+    if (is_array($op) && isset($op['callback']) && function_exists($op['callback'])) {
+      $args = array_merge($op['arguments'], array(&$_batch['results'], &$op_finished, &$op['sandbox']));
+      call_user_func_array($op['callback'], $args);
+    }
+    if ($op_finished == 1) {
+      array_shift($_batch['operations']);
+      // add the operations that were added during the execution
+      _batch_operations_insert_pending();
+      // Make sure this step isn't counted double
+      $op_finished = 0;
+    }
+    else {
+      // save back the sandbox
+      $_batch['operations'][key($_batch['operations'])] = $op;
+    }
+    if (timer_read('page') > 1000) {
+      break;
+    }
+  }
+
+  $remaining = count($_batch['operations']);
+  $total = $_batch['total'];
+  $current = $total - $remaining + $op_finished;
+  $percentage = floor($current / $total * 100);
+  $values = array(
+    '@remaining' => $remaining,
+    '@total' => $total,
+  );
+  $progress_message = t('Remaining @remaining of @total.', $values);
+
+  $message = $progress_message .'<br/>';
+  $message.= $task_message ? $task_message : '&nbsp';
+
+  return array($percentage, $message);
+}
+
+function _batch_finished($success) {
+  global $_batch;
+
+  // Set the flag for batch_shutdown() to clean the db record.
+  $_batch['running'] = FALSE;
+
+  // Call the 'finished' callback
+  // TODO : this should call _all_ finished callbacks
+  if (isset($_batch['finished_callback']) && function_exists($_batch['finished_callback'])) {
+    $_batch['finished_callback']($_batch, $success, $_batch['results']);
+  }
+
+  if (isset($_batch['destination'])) {
+    $_REQUEST['destination'] = $_batch['destination'];
+  }
+  $redirect = isset($_batch['redirect']) ? $_batch['redirect'] : '';
+  drupal_goto($redirect);
+}
+
+function batch_shutdown() {
+  global $_batch;
+  if ($_batch['running']) {
+    db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($_batch), $_batch['id']);
+  }
+  else {
+    db_query("DELETE FROM {batch} WHERE bid = %d", $_batch['id']);
+  }
+}
Index: misc/batch.js
===================================================================
RCS file: misc/batch.js
diff -N misc/batch.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/batch.js	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,31 @@
+if (Drupal.jsEnabled) {
+  $(document).ready(function() {
+    $('#progress').each(function () {
+      var holder = this;
+      var uri = Drupal.settings.batch.uri;
+      var initMessage = Drupal.settings.batch.initMessage;
+      var errorMessage = Drupal.settings.batch.errorMessage;
+
+      // Success: redirect to the summary.
+      var updateCallback = function (progress, status, pb) {
+        if (progress == 100) {
+          pb.stopMonitoring();
+          window.location = uri+'&op=finished';
+        }
+      }
+
+      var errorCallback = function (pb) {
+        var div = document.createElement('p');
+        div.className = 'error';
+        $(div).html(errorMessage);
+        $(holder).prepend(div);
+        $('#wait').hide();
+      }
+
+      var progress = new Drupal.progressBar('updateprogress', updateCallback, "POST", errorCallback);
+      progress.setProgress(-1, initMessage);
+      $(holder).append(progress.element);
+      progress.startMonitoring(uri+'&op=do', 10);
+    });
+  });
+}
