From c740ac7fd58b5f4597bde987ae9263f3d05febd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Hojtsy?= <gabor@hojtsy.hu>
Date: Fri, 4 May 2007 09:41:37 +0000
Subject: [PATCH] #127539: progressive operation support, refactoring
 update.php code to a generic batch API to support runnning operations in
 multiple HTTP requests   - update.php is already on the batch API   - node
 access rebuilding is in the works   - automatic locale importing is in the
 works

 Thanks to Yves Chedemois (yched) for the good code quality, very wide awareness of issues related to batches,
 and the fantastic turnaround times. Hats off.
---
 CHANGELOG.txt                    |   1 +
 includes/batch.inc               | 297 +++++++++++++++++++++++++++++++
 includes/common.inc              |   4 +-
 includes/form.inc                | 285 ++++++++++++++++++++++++++++-
 includes/theme.inc               |  13 +-
 misc/batch.js                    |  31 ++++
 misc/drupal.js                   |   3 +
 misc/progress.js                 |   6 +-
 modules/system/page.tpl.php      |   2 +-
 modules/system/system.install    |  18 ++
 modules/system/system.module     |  23 +++
 themes/bluemarine/page.tpl.php   |   2 +-
 themes/chameleon/chameleon.theme |   6 +-
 themes/garland/page.tpl.php      |   2 +-
 themes/pushbutton/page.tpl.php   |   2 +-
 update.php                       | 276 +++++++++++-----------------
 16 files changed, 769 insertions(+), 202 deletions(-)
 create mode 100644 includes/batch.inc
 create mode 100644 misc/batch.js

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index e3718cb477d4..c2815d1ef5a1 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -28,6 +28,7 @@ Drupal 6.0, xxxx-xx-xx (development version)
     * Added .info files to themes and made it easier to specify regions and features.
     * Added theme registry: modules can directly provide .tpl.php files for their themes without having to create theme_ functions.
     * Used the Garland theme for the installation and maintenance pages.
+- Refactored update.php to a generic batch API to be able to run time consuming operations in multiple subsequent HTTP requests
 
 Drupal 5.0, 2007-01-15
 ----------------------
diff --git a/includes/batch.inc b/includes/batch.inc
new file mode 100644
index 000000000000..60c0e370b6aa
--- /dev/null
+++ b/includes/batch.inc
@@ -0,0 +1,297 @@
+<?php
+
+/**
+ * @file Batch processing API for processes to run in multiple HTTP requests.
+ */
+
+/**
+ * State based dispatcher for batches.
+ */
+function _batch_page() {
+  global $user;
+
+  $batch =& batch_get();
+
+  if (isset($_REQUEST['id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d AND sid = %d", $_REQUEST['id'], $user->sid))) {
+    $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 'start':
+      $output = _batch_start();
+      break;
+
+    case 'do':
+      $output = _batch_do();
+      break;
+
+    case 'do_nojs':
+      $output = _batch_progress_page_nojs();
+      break;
+
+    case 'finished':
+      $output = _batch_finished();
+      break;
+  }
+
+  return $output;
+}
+
+/**
+ * Initiate the batch processing
+ */
+function _batch_start() {
+  // Choose between the JS and non-JS version.
+  // JS-enabled users are identified through the 'has_js' cookie set in drupal.js.
+  // If the user did not visit any JS enabled page during his browser session,
+  // he gets the non-JS version...
+  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() {
+  $batch = batch_get();
+  $current_set = _batch_current_set();
+
+  drupal_set_title($current_set['title']);
+  drupal_add_js('misc/progress.js', 'core', 'header');
+
+  $url = url($batch['url'], array('query' => array('id' => $batch['id'])));
+  $js_setting = array(
+    'batch' => array(
+      'errorMessage' => $current_set['error_message'] .'<br/>'. $batch['error_message'],
+      'initMessage' => $current_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() {
+  $batch =& batch_get();
+  $current_set = _batch_current_set();
+
+  drupal_set_title($current_set['title']);
+
+  $new_op = 'do_nojs';
+
+  if (!isset($batch['running'])) {
+    // This is the first page so return some output immediately.
+    $percentage = 0;
+    $message = $current_set['init_message'];
+    $batch['running'] = TRUE;
+  }
+  else {
+    // This is one of the later requests: do some processing 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 = $current_set['error_message'] .'<br/>'. $batch['error_message'];
+    $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';
+    }
+
+    // Processing 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;
+}
+
+/**
+ * Advance batch processing for 1 second (or process the whole batch if it
+ * was not set for progressive execution).
+ */
+function _batch_process() {
+  $batch =& batch_get();
+  $current_set =& _batch_current_set();
+
+  while (!$current_set['success']) {
+    $task_message = NULL;
+    $finished = 1;
+    if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) {
+      // Build the 'batch context' array, execute the function call, and retrieve the user message.
+      $batch_context = array('sandbox' => &$current_set['sandbox'], 'results' => &$current_set['results'], 'finished' => &$finished, 'message' => '');
+      call_user_func_array($function, array_merge($args, array(&$batch_context)));
+      $task_message = $batch_context['message'];
+    }
+    if ($finished == 1) {
+      // Make sure this step isn't counted double.
+      $finished = 0;
+      // Remove the operation, and clear the sandbox to reduce the stored data.
+      array_shift($current_set['operations']);
+      $current_set['sandbox'] = array();
+
+      // If the batch set is completed, browse through the remaining sets
+      // until we find one that acually has operations.
+      while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
+        $current_set =& _batch_current_set();
+      }
+    }
+    // Progressive mode : stop after 1 second
+    if ($batch['progressive'] && timer_read('page') > 1000) {
+      break;
+    }
+  }
+
+  if ($batch['progressive']) {
+    $remaining  = count($current_set['operations']);
+    $total      = $current_set['total'];
+    $current    = $total - $remaining + $finished;
+    $percentage = $total ? floor($current / $total * 100) : 100;
+    $values = array(
+      '@remaining'  => $remaining,
+      '@total'      => $total,
+      '@current'    => floor($current),
+      '@percentage' => $percentage,
+      );
+    $progress_message = strtr($current_set['progress_message'], $values);
+
+    $message = $progress_message .'<br/>';
+    $message.= $task_message ? $task_message : '&nbsp';
+
+    return array($percentage, $message);
+  }
+  else {
+    return _batch_finished();
+  }
+
+}
+
+/**
+ * Retrieve the batch set being currently processed.
+ */
+function &_batch_current_set() {
+  $batch =& batch_get();
+  return $batch['sets'][$batch['current_set']];
+}
+
+/**
+ * Move execution to the next batch set if any, executing the stored
+ * form _submit callbacks along the way (possibly inserting additional batch sets)
+ */
+function _batch_next_set() {
+  $batch =& batch_get();
+  if (isset($batch['sets'][$batch['current_set']+1])) {
+    $batch['current_set']++;
+    $current_set =& _batch_current_set();
+    if (isset($current_set['form submit']) && (list($function, $args) = $current_set['form submit']) && function_exists($function)) {
+      // We have to keep our own copy of $form_values, to account
+      // for possible alteration by the submit callback.
+      if (isset($batch['form_values'])) {
+        $args[1] = $batch['form_values'];
+      }
+      $redirect = call_user_func_array($function, $args);
+      // Store the form_values only if needed, to limit the
+      // amount of data we store in the batch.
+      if (isset($batch['sets'][$batch['current_set']+1])) {
+        $batch['form_values'] = $args[1];
+      }
+      if (isset($redirect)) {
+        $batch['redirect'] = $redirect;
+      }
+    }
+    return TRUE;
+  }
+}
+
+/**
+ * End the batch :
+ * Call the 'finished' callbacks to allow custom handling of results,
+ * and resolve page redirection.
+ */
+function _batch_finished() {
+  $batch =& batch_get();
+
+  // Execute 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'], $batch_set['operations']);
+    }
+  }
+
+  // Cleanup the batch table and unset the global $batch variable.
+  db_query("DELETE FROM {batch} WHERE bid = %d", $batch['id']);
+  $_batch = $batch;
+  $batch = NULL;
+
+  // Redirect if needed.
+  if ($_batch['progressive']) {
+    if (isset($_batch['destination'])) {
+      $_REQUEST['destination'] = $_batch['destination'];
+    }
+    $redirect = isset($_batch['redirect']) ? $_batch['redirect'] : $_batch['source_page'];
+    $form_redirect = isset($_batch['form_redirect']) ? $_batch['form_redirect'] : NULL;
+    // Let drupal_redirect_form handle redirection logic, using a bare pseudo form
+    // to limit the amount of data we store in the batch.
+    drupal_redirect_form(array('#redirect' => $form_redirect), $redirect);
+
+    // If we get here, $form['redirect']['#redirect'] was FALSE, and we are most
+    // probably dealing with a multistep form - not supported at the moment.
+    // Redirect to the originating page - first step of the form.
+    drupal_goto($_batch['source_page']);
+  }
+}
+
+/**
+ * Store tha batch data for next request, or clear the table if the batch is finished.
+ */
+function _batch_shutdown() {
+  if ($batch = batch_get()) {
+    db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($batch), $batch['id']);
+  }
+}
diff --git a/includes/common.inc b/includes/common.inc
index 354eb4f7f2ae..ec2f06432509 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2327,11 +2327,11 @@ function drupal_common_themes() {
       'arguments' => array('text' => NULL)
     ),
     'page' => array(
-      'arguments' => array('content' => NULL, 'show_blocks' => TRUE),
+      'arguments' => array('content' => NULL, 'show_blocks' => TRUE, 'show_messages' => TRUE),
       'file' => 'page',
     ),
     'maintenance_page' => array(
-      'arguments' => array('content' => NULL, 'messages' => TRUE),
+      'arguments' => array('content' => NULL, 'show_messages' => TRUE),
     ),
     'install_page' => array(
       'arguments' => array('content' => NULL),
diff --git a/includes/form.inc b/includes/form.inc
index dc843d2868c5..5de5ded31e19 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -255,6 +255,12 @@ function drupal_process_form($form_id, &$form) {
     // 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);
+      if ($batch =& batch_get()) {
+        $batch['progressive'] = !$form['#programmed'];
+        batch_process();
+        // Progressive batch processing redirects to the progress page.
+        // Execution continues only if programmed form.
+      }
       if (!$form['#programmed']) {
         drupal_redirect_form($form, $redirect);
       }
@@ -420,7 +426,19 @@ function drupal_submit_form($form_id, $form) {
         $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 =& batch_get()) {
+          // Some previous _submit callback has set a batch.
+          // We store the call in a special 'control' batch set for execution
+          // at the correct time during the batch processing workflow.
+          $batch['sets'][] = array('form submit' => array($function, $args));
+        }
+        else {
+          $redirect = call_user_func_array($function, $args);
+          if ($batch =& batch_get()) {
+            // The _submit callback has opened a batch: store the needed form info.
+            $batch['form_redirect'] = isset($form['#redirect']) ? $form['#redirect'] : NULL;
+          }
+        }
         $submitted = TRUE;
         if (isset($redirect)) {
           $goto = $redirect;
@@ -1491,14 +1509,14 @@ function theme_markup($element) {
 }
 
 /**
-* Format a password field.
-*
-* @param $element
-*   An associative array containing the properties of the element.
-*   Properties used:  title, value, description, size, maxlength, required, attributes
-* @return
-*   A themed HTML string representing the form.
-*/
+ * Format a password field.
+ *
+ * @param $element
+ *   An associative array containing the properties of the element.
+ *   Properties used:  title, value, description, size, maxlength, required, attributes
+ * @return
+ *   A themed HTML string representing the form.
+ */
 function theme_password($element) {
   $size = $element['#size'] ? ' size="'. $element['#size'] .'" ' : '';
   $maxlength = $element['#maxlength'] ? ' maxlength="'. $element['#maxlength'] .'" ' : '';
@@ -1625,3 +1643,252 @@ function form_clean_id($id = NULL) {
 /**
  * @} End of "defgroup form".
  */
+
+/**
+ * @defgroup batch Batch operations
+ * @{
+ * Functions allowing forms processing to be spread out over several page
+ * requests, thus ensuring that the processing does not get interrupted
+ * because of a PHP timeout, while allowing the user to receive feedback
+ * on the progress of the ongoing operations.
+ *
+ * The API is primarily designed to integrate nicely with the Form API
+ * workflow, but can also be used by non-FAPI scripts (like update.php)
+ * or even simple page callbacks (which should probably be used sparingly).
+ *
+ * Example:
+ * @code
+ * $batch = array(
+ *   'title' => t('Exporting'),
+ *   'operations' => array(
+ *     array('my_function_1', array($account->uid, 'story')),
+ *     array('my_function_2', array()),
+ *   ),
+ *   'finished' => 'my_finished_callback',
+ * );
+ * batch_set($batch);
+ * // only needed if not inside a form _submit callback :
+ * batch_process();
+ * @endcode
+ *
+ * Sample batch operations:
+ * @code
+ * // Simple and artificial: load a node of a given type for a given user
+ * function my_function_1($uid, $type, &$context) {
+ *   // The $context array gathers batch context information about the execution (read),
+ *   // as well as 'return values' for the current operation (write)
+ *   // The following keys are provided :
+ *   // 'results' (read / write): The array of results gathered so far by
+ *   //   the batch processing, for the curent operation to append its own.
+ *   // 'message' (write): A text message displayed in the progress page.
+ *   // The following keys allow for multi-step operations :
+ *   // 'sandbox' (read / write): An array that can be freely used to
+ *   //   store persistent data between iterations. It is recommended to
+ *   //   use this instead of $_SESSION, which is unsafe if the user
+ *   //   continues browsing in a separate window while the batch is processing.
+ *   // 'finished' (write): A float number between 0 and 1 informing
+ *   //   the processing engine of the completion level for the operation.
+ *   //   1 means the operation is finished and processing can continue
+ *   //   to the next operation. This value always be 1 if not specified
+ *   //   by the batch operation (a single-step operation), so that will
+ *   //   be considered as finished.
+ *   $node = node_load(array('uid' => $uid, 'type' => $type));
+ *   $context['results'][] = $node->nid .' : '. $node->title;
+ *   $context['message'] = $node->title;
+ * }
+ *
+ * // More advanced example: mutli-step operation - load all nodes, five by five
+ * function my_function_2(&$context) {
+ *   if (empty($context['sandbox'])) {
+ *     $context['sandbox']['progress'] = 0;
+ *     $context['sandbox']['current_node'] = 0;
+ *     $context['sandbox']['max'] = db_result(db_query('SELECT COUNT(DISTINCT nid) FROM {node}'));
+ *   }
+ *   $limit = 5;
+ *   $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit);
+ *   while ($row = db_fetch_array($result)) {
+ *     $node = node_load($row['nid'], NULL, TRUE);
+ *     $context['results'][] = $node->nid .' : '. $node->title;
+ *     $context['sandbox']['progress']++;
+ *     $context['sandbox']['current_node'] = $node->nid;
+ *     $context['message'] = $node->title;
+ *   }
+ *   if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ *     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+ *   }
+ * }
+ * @endcode
+ *
+ * Sample 'finished' callback:
+ * @code
+ * function batch_test_finished($success, $results, $operations) {
+ *   if ($success) {
+ *     $message = format_plural(count($results), 'One node processed.', '@count nodes processed.');
+ *   }
+ *   else {
+ *     $message = t('Finished with an error.');
+ *   }
+ *   drupal_set_message($message);
+ *   // Provinding data for the redirected page is done through $_SESSION.
+ *   foreach ($results as $result) {
+ *     $items[] = t('Loaded node %title.', array('%title' => $result));
+ *   }
+ *   $_SESSION['my_batch_results'] = $items;
+ * }
+ * @endcode
+ */
+
+/**
+ * Open a new batch.
+ *
+ * @param $batch
+ *   An array defining the batch. The following keys can be used:
+ *     'operations': an array of function calls to be performed.
+ *        Example:
+ *        @code
+ *        array(
+ *          array('my_function_1', array($arg1)),
+ *          array('my_function_2', array($arg2_1, $arg2_2)),
+ *        )
+ *        @endcode
+ *     All the other values below are optional.
+ *     batch_init() provides default values for the messages.
+ *     'title': title for the progress page.
+ *       Defaults to t('Processing').
+ *     'init_message': message displayed while the processing is initialized.
+ *       Defaults to t('Initializing.').
+ *     'progress_message': message displayed while processing the batch.
+ *       Available placeholders are @current, @remaining, @total and @percent.
+ *       Defaults to t('Remaining @remaining of @total.').
+ *     'error_message': message displayed if an error occurred while processing the batch.
+ *       Defaults to t('An error has occurred.').
+ *     'finished': the name of a function to be executed after the batch has completed.
+ *       This should be used to perform any result massaging that may be needed,
+ *       and possibly save data in $_SESSION for display after final page redirection.
+ *
+ * Operations are added as new batch sets. Batch sets are used to ensure
+ * clean code independency, ensuring that several batches submitted by
+ * different parts of the code (core / contrib modules) can be processed
+ * correctly while not interfering or having to cope with each other. Each
+ * batch set gets to specify his own UI messages, operates on it's own set
+ * of operations and results, and triggers it's own 'finished' callback.
+ * Batch sets are processed sequentially, with the progress bar starting
+ * fresh for every new set.
+ */
+function batch_set($batch_definition) {
+  if ($batch_definition) {
+    $batch =& batch_get();
+    // Initialize the batch
+    if (empty($batch)) {
+      $batch = array(
+        'id' => db_next_id('{batch}_bid'),
+        'sets' => array(),
+      );
+    }
+
+    $init = array(
+      'sandbox' => array(),
+      'results' => array(),
+      'success' => FALSE,
+    );
+    // Use get_t() to allow batches at install time.
+    $t = get_t();
+    $defaults = array(
+      'title' => $t('Processing'),
+      'init_message' => $t('Initializing.'),
+      'progress_message' => $t('Remaining @remaining of @total.'),
+      'error_message' => $t('An error has occurred.'),
+    );
+    $batch_set = $init + $batch_definition + $defaults;
+
+    // Tweak init_message to avoid the bottom of the page flickering down after init phase.
+    $batch_set['init_message'] .= '<br/>&nbsp;';
+    $batch_set['total'] = count($batch_set['operations']);
+
+    // If the batch is being processed (meaning we are executing a stored submit callback),
+    // insert the new set after the current one.
+    if (isset($batch['current_set'])) {
+      // array_insert does not exist...
+      $slice1 = array_slice($batch['sets'], 0, $batch['current_set'] + 1);
+      $slice2 = array_slice($batch['sets'], $batch['current_set'] + 1);
+      $batch['sets'] = array_merge($slice1, array($batch_set), $slice2);
+    }
+    else {
+      $batch['sets'][] = $batch_set;
+    }
+  }
+}
+
+/**
+ * Process the batch.
+ * 
+ * Unless the batch has been markes with 'progressive' = FALSE, the function
+ * isses a drupal_goto and thus ends page execution.
+ *
+ * This function is not needed in form submit callbacks; Form API takes care
+ * of batches issued during form submission.
+ *
+ * @param $redirect
+ *   (optional) Path to redirect to when the batch has finished processing.
+ * @param $url
+ *   (optional - should ony be used for separate scripts like update.php)
+ *   URL of the batch processing page.
+ */
+function batch_process($redirect = NULL, $url = NULL) {
+  global $form_values, $user;
+  $batch =& batch_get();
+
+  // batch_process should not be called inside form _submit callbacks, or while a
+  // batch is already running. Neutralize the call if it is the case.
+  if (isset($batch['current_set']) || (isset($form_values) && !isset($batch['progressive']))) {
+    return;
+  }
+
+  if (isset($batch)) {
+    // Add process information
+    $t = get_t();
+    $url = isset($url) ? $url : 'batch';
+    $process_info = array(
+      'current_set' => 0,
+      'progressive' => TRUE,
+      'url' => isset($url) ? $url : 'batch',
+      'source_page' => $_GET['q'],
+      'redirect' => $redirect,
+      'error_message' => $t('Please continue to <a href="!error_url">the error page</a>', array('!error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'error'))))),
+    );
+    $batch += $process_info;
+
+    if ($batch['progressive']) {
+      // Save and unset the destination if any. drupal_goto looks for redirection
+      // in $_REQUEST['destination'] and $_REQUEST['edit']['destination'].
+      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, sid, timestamp, batch) VALUES (%d, %d, %d, '%s')", $batch['id'], $user->sid, time(), serialize($batch));
+      drupal_goto($batch['url'], 'op=start&id='. $batch['id']);
+    }
+    else {
+      // Non-progressive execution: bypass the whole progressbar workflow
+      // and execute the batch in one pass.
+      require_once './includes/batch.inc';
+      _batch_process();
+    }
+  }
+}
+
+/**
+ * Retrive the current batch.
+ */
+function &batch_get() {
+  static $batch = array();
+  return $batch;
+}
+
+/**
+ * @} End of "defgroup batch".
+ */
diff --git a/includes/theme.inc b/includes/theme.inc
index 84e3406dd454..593c2819ef1c 100644
--- a/includes/theme.inc
+++ b/includes/theme.inc
@@ -686,10 +686,11 @@ function theme_placeholder($text) {
  *
  * @param $content
  *   The page content to show.
- * @param $messages
+ * @param $show_messages
  *   Whether to output status and error messages.
+ *   FALSE can be useful to postpone the messages to a subsequent page.
  */
-function theme_maintenance_page($content, $messages = TRUE) {
+function theme_maintenance_page($content, $show_messages = TRUE) {
   // Set required headers.
   drupal_set_header('Content-Type: text/html; charset=utf-8');
   drupal_set_html_head('<style type="text/css" media="all">@import "'. base_path() .'misc/maintenance.css";</style>');
@@ -710,7 +711,7 @@ function theme_maintenance_page($content, $messages = TRUE) {
     'logo' => base_path() .'themes/garland/minnelli/logo.png',
     'site_title' => t('Drupal'),
     'title' => drupal_get_title(),
-    'messages' => theme('status_messages'),
+    'messages' => $show_messages ? theme('status_messages') : '',
     'content' => $content,
   );
 
@@ -1288,9 +1289,9 @@ function theme_username($object) {
 
 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;
@@ -1399,7 +1400,7 @@ function template_preprocess_page(&$variables) {
   $variables['help']              = theme('help');
   $variables['language']          = $GLOBALS['language'];
   $variables['logo']              = theme_get_setting('logo');
-  $variables['messages']          = theme('status_messages');
+  $variables['messages']          = $variables['show_messages'] ? theme('status_messages') : '';
   $variables['mission']           = isset($mission) ? $mission : '';
   $variables['primary_links']     = menu_primary_links();
   $variables['search_box']        = (theme_get_setting('toggle_search') ? drupal_get_form('search_theme_form') : '');
diff --git a/misc/batch.js b/misc/batch.js
new file mode 100644
index 000000000000..43117e243147
--- /dev/null
+++ b/misc/batch.js
@@ -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);
+    });
+  });
+}
diff --git a/misc/drupal.js b/misc/drupal.js
index 04176089dd88..c4fa5d8ab186 100644
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -222,5 +222,8 @@ Drupal.getSelection = function (element) {
 
 // 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';
 }
diff --git a/misc/progress.js b/misc/progress.js
index 3db804f2f60e..cf5c120179e8 100644
--- a/misc/progress.js
+++ b/misc/progress.js
@@ -20,9 +20,9 @@ Drupal.progressBar = function (id, updateCallback, method, errorCallback) {
   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>');
 }
 
 /**
diff --git a/modules/system/page.tpl.php b/modules/system/page.tpl.php
index fce977e5fb36..5bf9e423b02e 100644
--- a/modules/system/page.tpl.php
+++ b/modules/system/page.tpl.php
@@ -41,7 +41,7 @@
         <h1 class="title"><?php print $title ?></h1>
         <div class="tabs"><?php print $tabs ?></div>
         <?php print $help ?>
-        <?php print $messages ?>
+        <?php if ($show_messages) { print $messages; }?>
         <?php print $content; ?>
         <?php print $feed_icons; ?>
       </div>
diff --git a/modules/system/system.install b/modules/system/system.install
index f409de7fa38f..7cf9b19797df 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -190,6 +190,15 @@ function system_install() {
         UNIQUE KEY authname (authname)
       ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
 
+      db_query("CREATE TABLE {batch} (
+        bid int(11) NOT NULL,
+        sid varchar(64) NOT NULL,
+        timestamp int(11) NOT NULL,
+        batch longtext,
+        PRIMARY KEY  (bid),
+        KEY sid (sid)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+
       db_query("CREATE TABLE {blocks} (
         module varchar(64) DEFAULT '' NOT NULL,
         delta varchar(32) NOT NULL default '0',
@@ -666,6 +675,15 @@ function system_install() {
         UNIQUE (authname)
       )");
 
+      db_query("CREATE TABLE {batch} (
+        bid int NOT NULL default '0',
+        sid varchar(64) NOT NULL default '',
+        timestamp int NOT NULL default '0',
+        batch text,
+        PRIMARY KEY (bid),
+      )");
+      db_query("CREATE INDEX {batch}_sid_idx ON {batch} (sid)");
+
       db_query("CREATE TABLE {blocks} (
         module varchar(64) DEFAULT '' NOT NULL,
         delta varchar(32) NOT NULL default '0',
diff --git a/modules/system/system.module b/modules/system/system.module
index fab91e2d6c7a..a18439e07a22 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -327,6 +327,12 @@ function system_menu() {
     'page callback' => 'system_sql',
     'type' => MENU_CALLBACK,
   );
+  // Default page for batch operations
+  $items['batch'] = array(
+    'page callback' => 'system_batch_page',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -2459,5 +2465,22 @@ function theme_system_admin_by_module($menu_items) {
 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() {
+  require_once './includes/batch.inc';
+  $output = _batch_page();
+  if ($output === FALSE) {
+    drupal_access_denied();
+  }
+  else {
+    // Force a page without blocks or messages to
+    // display a list of collected messages later.
+    print theme('page', $output, FALSE, FALSE);
+  }
+}
diff --git a/themes/bluemarine/page.tpl.php b/themes/bluemarine/page.tpl.php
index fce977e5fb36..2f101ddf17fa 100644
--- a/themes/bluemarine/page.tpl.php
+++ b/themes/bluemarine/page.tpl.php
@@ -41,7 +41,7 @@
         <h1 class="title"><?php print $title ?></h1>
         <div class="tabs"><?php print $tabs ?></div>
         <?php print $help ?>
-        <?php print $messages ?>
+        <?php if ($show_messages) { print $messages; } ?>
         <?php print $content; ?>
         <?php print $feed_icons; ?>
       </div>
diff --git a/themes/chameleon/chameleon.theme b/themes/chameleon/chameleon.theme
index 7d3134b86cbb..1c87653d3380 100644
--- a/themes/chameleon/chameleon.theme
+++ b/themes/chameleon/chameleon.theme
@@ -23,7 +23,7 @@ function chameleon_theme($existing) {
   return $templates;
 }
 
-function chameleon_page($content, $show_blocks = TRUE) {
+function chameleon_page($content, $show_blocks = TRUE, $show_messages = TRUE) {
   $language = isset($GLOBALS['language']) ? $GLOBALS['language']->language : NULL;
 
   if (theme_get_setting('toggle_favicon')) {
@@ -94,7 +94,9 @@ function chameleon_page($content, $show_blocks = TRUE) {
 
   $output .= theme('help');
 
-  $output .= theme('status_messages');
+  if ($show_messages) {
+    $output .= theme('status_messages');
+  }
 
   $output .= "\n<!-- begin content -->\n";
   $output .= $content;
diff --git a/themes/garland/page.tpl.php b/themes/garland/page.tpl.php
index 3663f6586a4a..5f6078228664 100644
--- a/themes/garland/page.tpl.php
+++ b/themes/garland/page.tpl.php
@@ -71,7 +71,7 @@
           <?php if (isset($tabs2)): print $tabs2; endif; ?>
 
           <?php if ($help): print $help; endif; ?>
-          <?php if ($messages): print $messages; endif; ?>
+          <?php if ($show_messages && $messages): print $messages; endif; ?>
           <?php print $content ?>
           <span class="clear"></span>
           <?php print $feed_icons ?>
diff --git a/themes/pushbutton/page.tpl.php b/themes/pushbutton/page.tpl.php
index acf27949bd44..027bd026ca46 100644
--- a/themes/pushbutton/page.tpl.php
+++ b/themes/pushbutton/page.tpl.php
@@ -76,7 +76,7 @@
             <div id="help"><?php print $help ?></div>
         <?php endif; ?>
 
-        <?php if ($messages != ""): ?>
+        <?php if ($show_messages && $messages != ""): ?>
           <?php print $messages ?>
         <?php endif; ?>
 
diff --git a/update.php b/update.php
index c104b2308007..c406531619c6 100644
--- a/update.php
+++ b/update.php
@@ -283,37 +283,34 @@ function update_fix_watchdog() {
  *   The module whose update will be run.
  * @param $number
  *   The update number to run.
- *
- * @return
- *   TRUE if the update was finished. Otherwise, FALSE.
+ * @param $context
+ *   The batch conetxt array
  */
-function update_data($module, $number) {
-  $ret = module_invoke($module, 'update_'. $number);
-  // Assume the update finished unless the update results indicate otherwise.
-  $finished = 1;
+function update_do_one($module, $number, &$context) {
+  $function = $module .'_update_'. $number;
+  if (function_exists($function)) {
+    $ret = $function(&$context['sandbox']);
+  }
+
   if (isset($ret['#finished'])) {
-    $finished = $ret['#finished'];
+    $context['finished'] = $ret['#finished'];
     unset($ret['#finished']);
   }
 
-  // Save the query and results for display by update_finished_page().
-  if (!isset($_SESSION['update_results'])) {
-    $_SESSION['update_results'] = array();
-  }
-  if (!isset($_SESSION['update_results'][$module])) {
-    $_SESSION['update_results'][$module] = array();
+  if (!isset($context['results'][$module])) {
+    $context['results'][$module] = array();
   }
-  if (!isset($_SESSION['update_results'][$module][$number])) {
-    $_SESSION['update_results'][$module][$number] = array();
+  if (!isset($context['results'][$module][$number])) {
+    $context['results'][$module][$number] = array();
   }
-  $_SESSION['update_results'][$module][$number] = array_merge($_SESSION['update_results'][$module][$number], $ret);
+  $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);;
 
-  if ($finished == 1) {
+  if ($context['finished'] == 1) {
     // Update the installed version
     drupal_set_installed_schema_version($module, $number);
   }
 
-  return $finished;
+  $context['message'] = t('Updating @module module', array('@module' => $module));
 }
 
 function update_selection_page() {
@@ -321,8 +318,6 @@ function update_selection_page() {
   $output .= '<p>Click Update to start the update process.</p>';
 
   drupal_set_title('Drupal database update');
-  // Prevent browser from using cached drupal.js or update.js
-  drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE);
   $output .= drupal_get_form('update_script_selection_form');
 
   update_task_list('select');
@@ -377,7 +372,10 @@ function update_script_selection_form() {
   return $form;
 }
 
-function update_update_page() {
+function update_batch() {
+  global $base_url;
+
+  $operations = array();
   // Set the installed version so updates start at the correct place.
   foreach ($_POST['start'] as $module => $version) {
     drupal_set_installed_schema_version($module, $version - 1);
@@ -386,145 +384,35 @@ function update_update_page() {
     if ($version <= $max_version) {
       foreach ($updates as $update) {
         if ($update >= $version) {
-          $_SESSION['update_remaining'][] = array('module' => $module, 'version' => $update);
+          $operations[] = array('update_do_one', array($module, $update));
         }
       }
     }
   }
-
-  // Keep track of total number of updates
-  if (isset($_SESSION['update_remaining'])) {
-    $_SESSION['update_total'] = count($_SESSION['update_remaining']);
-  }
-
-  if ($_POST['has_js']) {
-    return update_progress_page();
-  }
-  else {
-    return update_progress_page_nojs();
-  }
-}
-
-function update_progress_page() {
-  // Prevent browser from using cached drupal.js or update.js
-  drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE);
-  drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE);
-
-  drupal_set_title('Updating');
-  update_task_list('run');
-  $output = '<div id="progress"></div>';
-  $output .= '<p id="wait">Please wait while your site is being updated.</p>';
-  return $output;
-}
-
-/**
- * Perform updates for one second or until finished.
- *
- * @return
- *   An array indicating the status after doing updates. The first element is
- *   the overall percentage finished. The second element is a status message.
- */
-function update_do_updates() {
-  while (isset($_SESSION['update_remaining']) && ($update = reset($_SESSION['update_remaining']))) {
-    $update_finished = update_data($update['module'], $update['version']);
-    if ($update_finished == 1) {
-      // Dequeue the completed update.
-      unset($_SESSION['update_remaining'][key($_SESSION['update_remaining'])]);
-      $update_finished = 0; // Make sure this step isn't counted double
-    }
-    if (timer_read('page') > 1000) {
-      break;
-    }
-  }
-
-  if ($_SESSION['update_total']) {
-    $percentage = floor(($_SESSION['update_total'] - count($_SESSION['update_remaining']) + $update_finished) / $_SESSION['update_total'] * 100);
-  }
-  else {
-    $percentage = 100;
-  }
-
-  // When no updates remain, clear the caches in case the data has been updated.
-  if (!isset($update['module'])) {
-    cache_clear_all('*', 'cache', TRUE);
-    cache_clear_all('*', 'cache_page', TRUE);
-    cache_clear_all('*', 'cache_filter', TRUE);
-    drupal_clear_css_cache();
-  }
-
-  return array($percentage, isset($update['module']) ? 'Updating '. $update['module'] .' module' : 'Updating complete');
+  $batch = array(
+    'operations' => $operations,
+    'title' => 'Updating',
+    'init_message' => 'Starting updates',
+    'error_message' => 'An unrecoverable error has occured. You can find the error message below. It is advised to copy it to the clipboard for reference.',
+    'finished' => 'update_finished',
+  );
+  batch_set($batch);
+  batch_process($base_url .'/update.php?op=results', $base_url .'/update.php');
 }
 
-/**
- * Perform updates for the JS version and return progress.
- */
-function update_do_update_page() {
-  global $conf;
-
-  // HTTP Post required
-  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
-    drupal_set_message('HTTP Post is required.', 'error');
-    drupal_set_title('Error');
-    return '';
-  }
+function update_finished($success, $results, $operations) {
+  // clear the caches in case the data has been updated.
+  cache_clear_all('*', 'cache', TRUE);
+  cache_clear_all('*', 'cache_page', TRUE);
+  cache_clear_all('*', 'cache_filter', TRUE);
+  drupal_clear_css_cache();
 
-  // Error handling: if PHP dies, the output will fail to parse as JSON, and
-  // the Javascript will tell the user to continue to the op=error page.
-  list($percentage, $message) = update_do_updates();
-  print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+  $_SESSION['update_results'] = $results;
+  $_SESSION['update_success'] = $success;
+  $_SESSION['updates_remaining'] = $operations;
 }
 
-/**
- * Perform updates for the non-JS version and return the status page.
- */
-function update_progress_page_nojs() {
-  drupal_set_title('Updating');
-  update_task_list('run');
-
-  $new_op = 'do_update_nojs';
-  if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-    // This is the first page so return some output immediately.
-    $percentage = 0;
-    $message = 'Starting updates';
-  }
-  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. So, we put an explanation in the
-    // buffer to guide the user when an error happens.
-    ob_start();
-    $fallback = '<p class="error">An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference. Please continue to the <a href="update.php?op=error">update summary</a>.</p>';
-    $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;
-
-    // Do updates
-    list($percentage, $message) = update_do_updates();
-    if ($percentage == 100) {
-      $new_op = 'finished';
-    }
-
-    // Updates were successful; wipe the output buffer as it's unneeded.
-    ob_end_clean();
-  }
-
-  drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL=update.php?op='. $new_op .'">');
-  $output = theme('progress_bar', $percentage, $message);
-  $output .= '<p>Updating your site will take a few seconds.</p>';
-
-  // Note: do not output drupal_set_message()s until the summary page.
-  print theme('maintenance_page', $output, FALSE);
-  return NULL;
-}
-
-function update_finished_page($success) {
+function update_results_page() {
   drupal_set_title('Drupal database update');
   // NOTE: we can't use l() here because the URL would point to 'update.php?q=admin'.
   $links[] = '<a href="'. base_path() .'">Main page</a>';
@@ -532,18 +420,18 @@ function update_finished_page($success) {
 
   update_task_list();
   // Report end result
-  if ($success) {
+  if ($_SESSION['update_success']) {
     $output = '<p>Updates were attempted. If you see no failures below, you may proceed happily to the <a href="index.php?q=admin">administration pages</a>. Otherwise, you may need to update your database manually. All errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>.</p>';
   }
   else {
-    $update = reset($_SESSION['update_remaining']);
-    $output = '<p class="error">The update process was aborted prematurely while running <strong>update #'. $update['version'] .' in '. $update['module'] .'.module</strong>. All other errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>. You may need to check the <code>watchdog</code> database table manually.</p>';
+    list($module, $version) = array_pop(reset($_SESSION['updates_remaining']));
+    $output = '<p class="error">The update process was aborted prematurely while running <strong>update #'. $version .' in '. $module .'.module</strong>. All other errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>. You may need to check the <code>watchdog</code> database table manually.</p>';
   }
 
   if ($GLOBALS['access_check'] == FALSE) {
     $output .= "<p><strong>Reminder: don't forget to set the <code>\$access_check</code> value at the top of <code>update.php</code> back to <code>TRUE</code>.</strong></p>";
   }
-
+  
   $output .= theme('item_list', $links);
 
   // Output a list of queries executed
@@ -570,8 +458,9 @@ function update_finished_page($success) {
       }
     }
     $output .= '</div>';
-    unset($_SESSION['update_results']);
   }
+  unset($_SESSION['update_results']);
+  unset($_SESSION['update_success']);
 
   return $output;
 }
@@ -778,6 +667,45 @@ function update_create_cache_tables() {
   return $ret;
 }
 
+/**
+ * Create the batch table.
+ *
+ * This is part of the Drupal 5.x to 6.x migration.
+ */
+function update_create_batch_table() {
+
+  // If batch table exists, update is not necessary
+  if (db_table_exists('batch')) {
+    return;
+  }
+
+  $ret = array();
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      $ret[] = update_sql("CREATE TABLE {batch} (
+        bid int(11) NOT NULL,
+        sid varchar(64) NOT NULL,
+        timestamp int(11) NOT NULL,
+        batch longtext,
+        PRIMARY KEY  (bid),
+        KEY sid (sid)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+      break;
+    case 'pgsql':
+      $ret[] = update_sql("CREATE TABLE {batch} (
+        bid int NOT NULL default '0',
+        sid varchar(64) NOT NULL default '',
+        timestamp int NOT NULL default '0',
+        batch text,
+        PRIMARY KEY (bid),
+      )");
+      $ret[] = update_sql("CREATE INDEX {batch}_sid_idx ON {batch} (sid)");
+     break;
+  }
+  return $ret;
+}
+
 /**
  * Add the update task list to the current page.
  */
@@ -807,6 +735,7 @@ function update_task_list($active = NULL) {
 // variable_(get|set), which only works after a full bootstrap.
 update_fix_access_table();
 update_create_cache_tables();
+update_create_batch_table();
 
 // Turn error reporting back on. From now on, only fatal errors (which are
 // not passed through the error handler) will cause a message to be printed.
@@ -816,6 +745,7 @@ function update_task_list($active = NULL) {
 if (($access_check == FALSE) || ($user->uid == 1)) {
 
   include_once './includes/install.inc';
+  include_once './includes/batch.inc';
   drupal_load_updates();
 
   update_fix_schema_version();
@@ -825,39 +755,33 @@ function update_task_list($active = NULL) {
 
   $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
   switch ($op) {
-    case 'Update':
-      $output = update_update_page();
-      break;
-
-    case 'finished':
-      $output = update_finished_page(TRUE);
-      break;
-
-    case 'error':
-      $output = update_finished_page(FALSE);
+    // update.php ops
+    case '':
+      $output = update_info_page();
       break;
 
-    case 'do_update':
-      $output = update_do_update_page();
+    case 'selection':
+      $output = update_selection_page();
       break;
 
-    case 'do_update_nojs':
-      $output = update_progress_page_nojs();
+    case 'Update':
+      update_batch();
       break;
 
-    case 'selection':
-      $output = update_selection_page();
+    case 'results':
+      $output = update_results_page();
       break;
 
+    // Regular batch ops : defer to batch processing API
     default:
-      $output = update_info_page();
+      update_task_list('run');
+      $output = _batch_page();
       break;
   }
 }
 else {
   $output = update_access_denied_page();
 }
-
-if (isset($output)) {
+if (isset($output) && $output) {
   print theme('maintenance_page', $output);
 }
-- 
GitLab