diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 0b07d8e..f26c73f 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -14,49 +14,50 @@
  * @see batch_get()
  */
 
-use \Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Loads a batch from the database.
  *
  * @param $id
- *   The ID of the batch to load. When a progressive batch is being processed,
- *   the relevant ID is found in $_REQUEST['id'].
+ *   The ID of the batch to load.
  *
  * @return
  *   An array representing the batch, or FALSE if no batch was found.
  */
 function batch_load($id) {
-  $batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
+  $result = db_query('SELECT batch, token FROM {batch} WHERE bid = :bid', array(
     ':bid' => $id,
-    ':token' => drupal_get_token($id),
-  ))->fetchField();
-  if ($batch) {
-    return unserialize($batch);
+  ))->fetch();
+  if ($result) {
+    $batch = unserialize($result->batch);
+    $batch['token'] = $result->token;
+    return $batch;
   }
   return FALSE;
 }
 
 /**
+ * Access callback for the batch processing page.
+ *
+ * @param $batch
+ *   A loaded batch record, as returned by batch_load().
+ */
+function _batch_page_access($batch) {
+  return isset($batch['token']) && $batch['token'] === drupal_get_token($batch['id']);
+}
+
+/**
  * Renders the batch processing page based on the current state of the batch.
  *
  * @see _batch_shutdown()
  */
-function _batch_page() {
+function _batch_page($loaded_batch) {
   $batch = &batch_get();
-
-  if (!isset($_REQUEST['id'])) {
-    return FALSE;
-  }
-
-  // Retrieve the current state of the batch.
-  if (!$batch) {
-    $batch = batch_load($_REQUEST['id']);
-    if (!$batch) {
-      drupal_set_message(t('No active batch.'), 'error');
-      drupal_goto();
-    }
-  }
+  $batch = $loaded_batch;
 
   // Register database update for the end of processing.
   drupal_register_shutdown_function('_batch_shutdown');
@@ -78,9 +79,8 @@ function _batch_page() {
       break;
 
     case 'do':
-      // JavaScript-based progress page callback.
-      $output = _batch_do();
-      break;
+      // Return a JSON response for JavaScript-based progress page callback.
+      return _batch_do();
 
     case 'do_nojs':
       // Non-JavaScript-based progress page.
@@ -92,7 +92,12 @@ function _batch_page() {
       break;
   }
 
-  return $output;
+  // Force a page without blocks or messages to display a list of collected
+  // messages later.
+  drupal_set_page_content($output);
+  $page = element_info('page');
+  $page['#show_messages'] = FALSE;
+  return $page;
 }
 
 /**
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 6a747b6..ab35019 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -4835,6 +4835,11 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd
     // table, since non-progressive batches skip database storage completely.
     $batch['id'] = db_next_id();
 
+    // Append the batch ID to the path for the default /batch callback.
+    if ($batch['url'] == 'batch') {
+      $batch['url'] = $batch['url'] . '/' . $batch['id'];
+    }
+
     // Move operations to a job queue. Non-progressive batches will use a
     // memory-based queue.
     foreach ($batch['sets'] as $key => $batch_set) {
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 889dbe2..f4458dd 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -487,8 +487,16 @@ function install_run_task($task, &$install_state) {
     // If we are in the middle of processing this batch, keep sending back
     // any output from the batch process, until the task is complete.
     elseif ($current_batch == $function) {
+      // The task is complete when we are at a URL without a batch ID.
+      if (empty($_REQUEST['id'])) {
+        // Return nothing so the next task will run in the same request.
+        variable_del('install_current_batch');
+        return;
+      }
+
       include_once DRUPAL_ROOT . '/core/includes/batch.inc';
-      $output = _batch_page();
+      $batch = batch_load($_REQUEST['id']);
+      $output = _batch_page($batch);
       // Because Batch API now returns a JSON response for intermediary steps,
       // but the installer doesn't handle Response objects yet, just send the
       // output here and emulate the old model.
@@ -498,21 +506,16 @@ function install_run_task($task, &$install_state) {
         $output->send();
         $output = NULL;
       }
-      // The task is complete when we try to access the batch page and receive
-      // FALSE in return, since this means we are at a URL where we are no
-      // longer requesting a batch ID.
-      if ($output === FALSE) {
-        // Return nothing so the next task will run in the same request.
-        variable_del('install_current_batch');
-        return;
-      }
-      else {
-        // We need to force the page request to end if the task is not
-        // complete, since the batch API sometimes prints JSON output
-        // rather than returning a themed page.
-        $install_state['task_not_complete'] = $install_state['stop_page_request'] = TRUE;
-        return $output;
+      elseif (is_array($output)) {
+        // _batch_page() sets the page content and returns a #type page.
+        $output = drupal_set_page_content();
+        // If the actual page output is NULL, then the batch is complete.
+        $output = drupal_render($output);
       }
+      // Force the page request to end if the task is not complete, since Batch
+      // API might return JSON output instead of themed page content.
+      $install_state['task_not_complete'] = $install_state['stop_page_request'] = TRUE;
+      return $output;
     }
   }
 
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 5677369..9a54f00 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -501,6 +501,9 @@ function _menu_load_objects(&$item, &$map) {
     if (!is_array($load_functions)) {
       $load_functions = unserialize($load_functions);
     }
+    if (!empty($item['include_file'])) {
+      include_once DRUPAL_ROOT . '/' . $item['include_file'];
+    }
     $path_map = $map;
     foreach ($load_functions as $index => $function) {
       if ($function) {
@@ -3465,6 +3468,12 @@ function _menu_router_build($callbacks) {
   $menu = array();
   $masks = array();
   foreach ($callbacks as $path => $item) {
+    // Load the defined include file, if any, so callbacks are defined.
+    if (!empty($item['file'])) {
+      $file_path = !empty($item['file path']) ? $item['file path'] : drupal_get_path('module', $item['module']);
+      include_once DRUPAL_ROOT . '/' . $file_path . '/' . $item['file'];
+    }
+
     $load_functions = array();
     $to_arg_functions = array();
     $fit = 0;
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index d956f05..2c66a1c 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -2293,29 +2293,6 @@ function system_php() {
 }
 
 /**
- * Default page callback for batches.
- */
-function system_batch_page() {
-  require_once DRUPAL_ROOT . '/core/includes/batch.inc';
-  $output = _batch_page();
-
-  if ($output === FALSE) {
-    throw new AccessDeniedHttpException();
-  }
-  elseif ($output instanceof Response) {
-    return $output;
-  }
-  elseif (isset($output)) {
-    // Force a page without blocks or messages to
-    // display a list of collected messages later.
-    drupal_set_page_content($output);
-    $page = element_info('page');
-    $page['#show_messages'] = FALSE;
-    return $page;
-  }
-}
-
-/**
  * Returns HTML for an administrative block for display.
  *
  * @param $variables
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 14cc390..f882267 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1114,12 +1114,16 @@ function system_menu() {
   );
 
   // Default page for batch operations.
-  $items['batch'] = array(
-    'page callback' => 'system_batch_page',
-    'access callback' => TRUE,
+  $items['batch/%batch'] = array(
+    'page callback' => '_batch_page',
+    'page arguments' => array(1),
+    'access callback' => '_batch_page_access',
+    'access arguments' => array(1),
     'theme callback' => '_system_batch_theme',
+    'theme arguments' => array(1),
     'type' => MENU_CALLBACK,
-    'file' => 'system.admin.inc',
+    'file path' => 'core/includes',
+    'file' => 'batch.inc',
   );
 
   // Localize date formats.
@@ -1192,13 +1196,7 @@ function system_cron_access($key) {
 /**
  * Theme callback for the default batch page.
  */
-function _system_batch_theme() {
-  // Retrieve the current state of the batch.
-  $batch = &batch_get();
-  if (!$batch && isset($_REQUEST['id'])) {
-    require_once DRUPAL_ROOT . '/core/includes/batch.inc';
-    $batch = batch_load($_REQUEST['id']);
-  }
+function _system_batch_theme($batch) {
   // Use the same theme as the page that started the batch.
   if (!empty($batch['theme'])) {
     return $batch['theme'];
