diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index e3718cb477d458b571c551b8005e482f47f30971..c2815d1ef5a11b6c5ae62f06a1ab7e062d9f26c3 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 0000000000000000000000000000000000000000..60c0e370b6aae255436a059c413c4c004009cf9a
--- /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 354eb4f7f2ae0fd79c4875c43855bd59cfa244b4..ec2f064325099b57a484cb82bf3ea5b9a4aa7423 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 dc843d2868c57e42efda7e00d56ca55363bcf227..5de5ded31e1908bb4ed02a13d98724ad6dd526db 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 84e3406dd454304787c202aa14c72a452e48f5f5..593c2819ef1c3dee714bf1f00affe49872fa58c8 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 0000000000000000000000000000000000000000..43117e2431476bc9e4f927fb6349afebdd99e268
--- /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 04176089dd887fa59dc2880581bbe01e23a9ed69..c4fa5d8ab18694bea47187d742039e08f2f3b977 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 3db804f2f60e6ad746cddb4ae5966e76c3d40ed6..cf5c120179e8788a3805d97bbca3de642aef8d3f 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 fce977e5fb36203e5380a051543ad1df8eaf0d13..5bf9e423b02e09bbb7fa6206239cd45de3934760 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 f409de7fa38f96930f18a263e06ec125bca62261..7cf9b19797df57e82dc04780331b20e958a7e519 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 fab91e2d6c7a9d3e96984b70d53d58e3bf2af69a..a18439e07a2230e8add71e53530aa40885e24668 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 fce977e5fb36203e5380a051543ad1df8eaf0d13..2f101ddf17facb8f62ec54a6a119f573e35a8386 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 7d3134b86cbb899db13ba9c412ab31a4ba5dca59..1c87653d3380f55741ea89dad84b4951c20f03d1 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 3663f6586a4abdfd05d12626f8b10e195e54755c..5f607822866456b67cb4753d1363c5e01e0eb6aa 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 acf27949bd441c2dbe7033c47b8eab1f4115e802..027bd026ca467f790ce7fa58fc634e4f6a5dc14a 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 c104b2308007977b4da33173de5a264cdf97076e..c406531619c63a0eb77a8d0f598da739ed381a9a 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);
 }