diff --git a/includes/batch.inc b/includes/batch.inc
index 16a5326caed59496cb19053ad27d513a5ac4f425..3f93bcbbcea4d5c48ab5fdd4e51be5e5ff7c46b4 100644
--- a/includes/batch.inc
+++ b/includes/batch.inc
@@ -22,13 +22,14 @@ function _batch_page() {
   register_shutdown_function('_batch_shutdown');
 
   $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+  $output = NULL;
   switch ($op) {
     case 'start':
       $output = _batch_start();
       break;
 
     case 'do':
-      $output = _batch_do();
+      _batch_do();
       break;
 
     case 'do_nojs':
@@ -97,9 +98,7 @@ function _batch_do() {
 
   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();
+  drupal_json(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
 }
 
 /**
@@ -159,6 +158,10 @@ function _batch_process() {
   $batch =& batch_get();
   $current_set =& _batch_current_set();
 
+  if ($batch['progressive']) {
+    timer_start('batch_processing');
+  }
+
   while (!$current_set['success']) {
     $finished = 1;
     $task_message = '';
@@ -169,33 +172,51 @@ function _batch_process() {
     }
 
     if ($finished == 1) {
-      // Make sure this step isn't counted double.
+      // Make sure this step isn't counted double when computing $current.
       $finished = 0;
-      // Remove the operation, and clear the sandbox to reduce the stored data.
+      // Remove the operation and clear the sandbox.
       array_shift($current_set['operations']);
       $current_set['sandbox'] = array();
     }
 
-    // Make sure we display progress information about a batch set that
-    // actually has operations, and not about a 'control' set (form submit
-    // handler).
-    $remaining = count($current_set['operations']);
-    $progress_message = $current_set['progress_message'];
-    $total = $current_set['total'];
-
-    // If the batch set is completed, browse through the remaining sets
-    // until we find one that actually has operations.
+    // If the batch set is completed, browse through the remaining sets,
+    // executing 'control sets' (stored submit handlers) along the way - this
+    // might in turn insert new batch sets. Stop when we find a set that
+    // actually has operations.
+    $set_changed = FALSE;
+    $old_set = $current_set;
     while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
       $current_set =& _batch_current_set();
+      $set_changed = TRUE;
     }
+    // At this point, either $current_set is a 'real' batch set (has operations),
+    // or all sets have been completed.
 
-    // Progressive mode : stop after 1 second
-    if ($batch['progressive'] && timer_read('page') > 1000) {
+    // Progressive mode : stop after 1 second.
+    if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
       break;
     }
   }
 
   if ($batch['progressive']) {
+    // Gather progress information.
+
+    // Reporting 100% progress will cause the whole batch to be considered
+    // processed. If processing was paused right after moving to a new set,
+    // we have to use the info from the new one.
+    if ($set_changed && isset($current_set['operations'])) {
+      // Processing will continue with a fresh batch set.
+      $remaining = count($current_set['operations']);
+      $total = $current_set['total'];
+      $progress_message = $current_set['init_message'];
+      $task_message = '';
+    }
+    else {
+      $remaining = count($old_set['operations']);
+      $total = $old_set['total'];
+      $progress_message = $old_set['progress_message'];
+    }
+
     $current    = $total - $remaining + $finished;
     $percentage = $total ? floor($current / $total * 100) : 100;
     $values = array(
@@ -258,7 +279,9 @@ function _batch_finished() {
   }
 
   // Cleanup the batch table and unset the global $batch variable.
-  db_query("DELETE FROM {batch} WHERE bid = %d", $batch['id']);
+  if ($batch['progressive']) {
+    db_query("DELETE FROM {batch} WHERE bid = %d", $batch['id']);
+  }
   $_batch = $batch;
   $batch = NULL;
 
diff --git a/includes/form.inc b/includes/form.inc
index 92d4fe4016a9beeeb728f9c5b7f3c8df6f4b9178..865c1fecdee887e57b7658a789d2e83b87e888f1 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -1398,6 +1398,8 @@ function expand_date($element) {
         $options = drupal_map_assoc(range(1900, 2050));
         break;
     }
+    $parents = $element['#parents'];
+    $parents[] = $type;
     $element[$type] = array(
       '#type' => 'select',
       '#value' => $element['#value'][$type],
@@ -2027,7 +2029,7 @@ function batch_set($batch_definition) {
  * isses a drupal_goto and thus ends page execution.
  *
  * This function is not needed in form submit handlers; Form API takes care
- * of batches issued during form submission.
+ * of batches that were set during form submission.
  *
  * @param $redirect
  *   (optional) Path to redirect to when the batch has finished processing.
@@ -2040,7 +2042,6 @@ function batch_process($redirect = NULL, $url = NULL) {
 
   if (isset($batch)) {
     // Add process information
-    $t = get_t();
     $url = isset($url) ? $url : 'batch';
     $process_info = array(
       'current_set' => 0,
@@ -2048,13 +2049,13 @@ function batch_process($redirect = NULL, $url = NULL) {
       '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'].
+      // Clear the way for the drupal_goto redirection to the batch processing
+      // page, by saving and unsetting the 'destination' if any, on both places
+      // drupal_goto looks for it.
       if (isset($_REQUEST['destination'])) {
         $batch['destination'] = $_REQUEST['destination'];
         unset($_REQUEST['destination']);
@@ -2063,9 +2064,20 @@ function batch_process($redirect = NULL, $url = NULL) {
         $batch['destination'] = $_REQUEST['edit']['destination'];
         unset($_REQUEST['edit']['destination']);
       }
-      db_query('INSERT INTO {batch} (timestamp) VALUES (%d)', time());
+
+      // Initiate db storage in order to get a batch id. We have to provide
+      // at least an empty string for the (not null) 'token' column.
+      db_query("INSERT INTO {batch} (token, timestamp) VALUES ('', %d)", time());
       $batch['id'] = db_last_insert_id('batch', 'bid');
+
+      // Now that we have a batch id, we can generate the redirection link in
+      // the generic error message.
+      $t = get_t();
+      $batch['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' => 'finished')))));
+
+      // Actually store the batch data and the token generated form the batch id.
       db_query("UPDATE {batch} SET token = '%s', batch = '%s' WHERE bid = %d", drupal_get_token($batch['id']), serialize($batch), $batch['id']);
+
       drupal_goto($batch['url'], 'op=start&id='. $batch['id']);
     }
     else {
diff --git a/modules/system/system.module b/modules/system/system.module
index 59a13ee1d3ab48c9d6c42e999283e2f664535b03..0ebaaea517b107b10d195d44ab184f09c4b82adf 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -2808,7 +2808,7 @@ function system_batch_page() {
   if ($output === FALSE) {
     drupal_access_denied();
   }
-  else {
+  elseif (isset($output)) {
     // Force a page without blocks or messages to
     // display a list of collected messages later.
     print theme('page', $output, FALSE, FALSE);