From 06e1dd8b0f5229d4041ed5f1b9750b8327366b4b Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Wed, 31 Jul 2013 14:00:23 -0700
Subject: [PATCH] Issue #1987602 by glennpratt, juampy, pwieck, vijaycs85,
 dawehner, tim.plunkett, effulgentsia: Convert ajax_form_callback() to a new
 style controller.

---
 core/includes/ajax.inc                        | 119 +++---------------
 core/includes/form.inc                        |  14 ++-
 core/modules/field/field.form.inc             |   4 +-
 core/modules/file/file.field.inc              |   9 +-
 core/modules/file/file.module                 | 112 ++---------------
 core/modules/file/file.routing.yml            |  12 ++
 .../Controller/FileWidgetAjaxController.php   | 119 ++++++++++++++++++
 .../system/Controller/FormAjaxController.php  | 105 ++++++++++++++++
 .../system/Tests/Ajax/FormValuesTest.php      |  10 ++
 core/modules/system/system.module             |   5 +-
 core/modules/system/system.routing.yml        |   6 +
 .../ajax_forms_test/ajax_forms_test.module    |  16 +++
 12 files changed, 315 insertions(+), 216 deletions(-)
 create mode 100644 core/modules/file/file.routing.yml
 create mode 100644 core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php
 create mode 100644 core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php

diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 8e024a8d3afc..1bced4475beb 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -23,9 +23,9 @@
  * forms, it can be used with the #ajax property.
  * The #ajax property can be used to bind events to the Ajax framework. By
  * default, #ajax uses 'system/ajax' as its path for submission and thus calls
- * ajax_form_callback() and a defined #ajax['callback'] function.
- * However, you may optionally specify a different path to request or a
- * different callback function to invoke, which can return updated HTML or can
+ * \Drupal\system\FormAjaxController::content() and a defined #ajax['callback']
+ * function. However, you may optionally specify a different path to request or
+ * a different callback function to invoke, which can return updated HTML or can
  * also return a richer set of
  * @link ajax_commands Ajax framework commands @endlink.
  *
@@ -38,17 +38,18 @@
  *     that element.
  *   - The browser submits an HTTP POST request to the 'system/ajax' Drupal
  *     path.
- *   - The menu page callback for 'system/ajax', ajax_form_callback(), calls
- *     drupal_process_form() to process the form submission and rebuild the
- *     form if necessary. The form is processed in much the same way as if it
- *     were submitted without Ajax, with the same #process functions and
- *     validation and submission handlers called in either case, making it easy
- *     to create Ajax-enabled forms that degrade gracefully when JavaScript is
- *     disabled.
- *   - After form processing is complete, ajax_form_callback() calls the
- *     function named by #ajax['callback'], which returns the form element that
- *     has been updated and needs to be returned to the browser, or
- *     alternatively, an array of custom Ajax commands.
+ *   - The controller for the route '/system/ajax',
+ *     \Drupal\system\FormAjaxController::content(), calls drupal_process_form()
+ *     to process the form submission and rebuild the form if necessary. The
+ *     form is processed in much the same way as if it were submitted without
+ *     Ajax, with the same #process functions and validation and submission
+ *     handlers called in either case, making it easy to create Ajax-enabled
+ *     forms that degrade gracefully when JavaScript is disabled.
+ *   - After form processing is complete,
+ *     \Drupal\system\FormAjaxController::content() calls the function named by
+ *     #ajax['callback'], which returns the form element that has been updated
+ *     and needs to be returned to the browser, or alternatively, an array of
+ *     custom Ajax commands.
  *   - The array is serialized using ajax_render() and sent to the browser.
  *   - The browser unserializes the returned JSON string into an array of
  *     command objects and executes each command, resulting in the old page
@@ -125,10 +126,10 @@
  * - #ajax['path']: The menu path to use for the request. This is often omitted
  *   and the default is used. This path should map
  *   to a menu page callback that returns data using ajax_render(). Defaults to
- *   'system/ajax', which invokes ajax_form_callback(), eventually calling
- *   the function named in #ajax['callback']. If you use a custom
- *   path, you must set up the menu entry and handle the entire callback in your
- *   own code.
+ *   'system/ajax', which invokes \Drupal\system\FormAjaxController::content(),
+ *   eventually calling the function named in #ajax['callback']. If you use a
+ *   custom path, you must set up the menu entry and handle the entire callback
+ *   in your own code.
  * - #ajax['wrapper']: The CSS ID of the area to be replaced by the content
  *   returned by the #ajax['callback'] function. The content returned from
  *   the callback will replace the entire element named by #ajax['wrapper'].
@@ -301,88 +302,6 @@ function ajax_render($commands = array()) {
   return drupal_json_encode($commands);
 }
 
-/**
- * Gets a form submitted via #ajax during an Ajax callback.
- *
- * This will load a form from the form cache used during Ajax operations. It
- * pulls the form info from $_POST.
- *
- * @return
- *   An array containing the $form and $form_state. Use the list() function
- *   to break these apart:
- *   @code
- *     list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
- *   @endcode
- */
-function ajax_get_form() {
-  $form_state = form_state_defaults();
-
-  $form_build_id = $_POST['form_build_id'];
-
-  // Get the form from the cache.
-  $form = form_get_cache($form_build_id, $form_state);
-  if (!$form) {
-    // If $form cannot be loaded from the cache, the form_build_id in $_POST
-    // must be invalid, which means that someone performed a POST request onto
-    // system/ajax without actually viewing the concerned form in the browser.
-    // This is likely a hacking attempt as it never happens under normal
-    // circumstances, so we just do nothing.
-    watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
-    throw new BadRequestHttpException();
-  }
-
-  // Since some of the submit handlers are run, redirects need to be disabled.
-  $form_state['no_redirect'] = TRUE;
-
-  // When a form is rebuilt after Ajax processing, its #build_id and #action
-  // should not change.
-  // @see drupal_rebuild_form()
-  $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
-  $form_state['rebuild_info']['copy']['#action'] = TRUE;
-
-  // The form needs to be processed; prepare for that by setting a few internal
-  // variables.
-  $form_state['input'] = $_POST;
-  $form_id = $form['#form_id'];
-
-  return array($form, $form_state, $form_id, $form_build_id);
-}
-
-/**
- * Page callback: Handles Ajax requests for the #ajax Form API property.
- *
- * This rebuilds the form from cache and invokes the defined #ajax['callback']
- * to return an Ajax command structure for JavaScript. In case no 'callback' has
- * been defined, nothing will happen.
- *
- * The Form API #ajax property can be set both for buttons and other input
- * elements.
- *
- * This function is also the canonical example of how to implement
- * #ajax['path']. If processing is required that cannot be accomplished with
- * a callback, re-implement this function and set #ajax['path'] to the
- * enhanced function.
- *
- * @see system_menu()
- */
-function ajax_form_callback() {
-  list($form, $form_state) = ajax_get_form();
-  drupal_process_form($form['#form_id'], $form, $form_state);
-
-  // We need to return the part of the form (or some other content) that needs
-  // to be re-rendered so the browser can update the page with changed content.
-  // Since this is the generic menu callback used by many Ajax elements, it is
-  // up to the #ajax['callback'] function of the element (may or may not be a
-  // button) that triggered the Ajax request to determine what needs to be
-  // rendered.
-  if (!empty($form_state['triggering_element'])) {
-    $callback = $form_state['triggering_element']['#ajax']['callback'];
-  }
-  if (!empty($callback) && is_callable($callback)) {
-    return call_user_func_array($callback, array(&$form, &$form_state));
-  }
-}
-
 /**
  * Theme callback: Returns the correct theme for an Ajax request.
  *
diff --git a/core/includes/form.inc b/core/includes/form.inc
index a9b83ae4346a..72ba2b40c323 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -471,8 +471,9 @@ function form_state_defaults() {
  * workflow, to be returned for rendering.
  *
  * Ajax form submissions are almost always multi-step workflows, so that is one
- * common use-case during which form rebuilding occurs. See ajax_form_callback()
- * for more information about creating Ajax-enabled forms.
+ * common use-case during which form rebuilding occurs. See
+ * Drupal\system\FormAjaxController::content() for more information about
+ * creating Ajax-enabled forms.
  *
  * @param $form_id
  *   The unique string identifying the desired form. If a function
@@ -496,7 +497,7 @@ function form_state_defaults() {
  *   The newly built form.
  *
  * @see drupal_process_form()
- * @see ajax_form_callback()
+ * @see \Drupal\system\FormAjaxController::content()
  */
 function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   $form = drupal_retrieve_form($form_id, $form_state);
@@ -1292,9 +1293,10 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
  * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
  *   redirection is done.
  * - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
- *   set, for instance, by ajax_get_form() to prevent redirection in Ajax
- *   callbacks. $form_state['no_redirect'] should never be set or altered by
- *   form builder functions or form validation/submit handlers.
+ *   set, for instance, by \Drupal\system\FormAjaxController::getForm() to
+ *   prevent redirection in Ajax callbacks. $form_state['no_redirect'] should
+ *   never be set or altered by form builder functions or form validation/submit
+ *   handlers.
  * - If $form_state['redirect'] is set to FALSE, redirection is disabled.
  * - If none of the above conditions has prevented redirection, then the
  *   redirect is accomplished by returning a RedirectResponse, passing in the
diff --git a/core/modules/field/field.form.inc b/core/modules/field/field.form.inc
index ac0cb701d2ab..84571ebcf3c7 100644
--- a/core/modules/field/field.form.inc
+++ b/core/modules/field/field.form.inc
@@ -129,8 +129,8 @@ function field_form_element_after_build($element, &$form_state) {
  * This handler is run regardless of whether JS is enabled or not. It makes
  * changes to the form state. If the button was clicked with JS disabled, then
  * the page is reloaded with the complete rebuilt form. If the button was
- * clicked with JS enabled, then ajax_form_callback() calls field_add_more_js()
- * to return just the changed part of the form.
+ * clicked with JS enabled, then Drupal\system\FormAjaxController::content()
+ * calls field_add_more_js() to return just the changed part of the form.
  */
 function field_add_more_submit($form, &$form_state) {
   $button = $form_state['triggering_element'];
diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc
index 7e693c79b2be..d3dc0a09c77c 100644
--- a/core/modules/file/file.field.inc
+++ b/core/modules/file/file.field.inc
@@ -428,12 +428,19 @@ function file_field_widget_process($element, &$form_state, $form) {
   // file, the entire group of file fields is updated together.
   if ($element['#cardinality'] != 1) {
     $parents = array_slice($element['#array_parents'], 0, -1);
-    $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
+    $new_path = 'file/ajax';
+    $new_options = array(
+      'query' => array(
+        'element_parents' => implode('/', $parents),
+        'form_build_id' => $form['form_build_id']['#value'],
+      ),
+    );
     $field_element = NestedArray::getValue($form, $parents);
     $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
     foreach (element_children($element) as $key) {
       if (isset($element[$key]['#ajax'])) {
         $element[$key]['#ajax']['path'] = $new_path;
+        $element[$key]['#ajax']['options'] = $new_options;
         $element[$key]['#ajax']['wrapper'] = $new_wrapper;
       }
     }
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index c662493ea227..9858bd08815e 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -46,14 +46,12 @@ function file_menu() {
   $items = array();
 
   $items['file/ajax'] = array(
-    'page callback' => 'file_ajax_upload',
-    'access arguments' => array('access content'),
+    'route_name' => 'file.ajax.upload',
     'theme callback' => 'ajax_base_page_theme',
     'type' => MENU_CALLBACK,
   );
   $items['file/progress'] = array(
-    'page callback' => 'file_ajax_progress',
-    'access arguments' => array('access content'),
+    'route name' => 'file.ajax.progress',
     'theme callback' => 'ajax_base_page_theme',
     'type' => MENU_CALLBACK,
   );
@@ -959,104 +957,6 @@ function file_save_upload($form_field_name, $validators = array(), $destination
   return isset($delta) ? $files[$delta] : $files;
 }
 
-/**
- * Ajax callback: Processes file uploads and deletions.
- *
- * This rebuilds the form element for a particular field item. As long as the
- * form processing is properly encapsulated in the widget element the form
- * should rebuild correctly using FAPI without the need for additional callbacks
- * or processing.
- *
- * @see file_menu()
- */
-function file_ajax_upload() {
-  $form_parents = func_get_args();
-  $form_build_id = (string) array_pop($form_parents);
-
-  $request = \Drupal::request();
-  if (!$request->request->has('form_build_id') || $form_build_id != $request->request->get('form_build_id')) {
-    // Invalid request.
-    drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
-    $response = new AjaxResponse();
-    $status_messages = array('#theme' => 'status_messages');
-    return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
-  }
-
-  list($form, $form_state) = ajax_get_form();
-
-  if (!$form) {
-    // Invalid form_build_id.
-    drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
-    $response = new AjaxResponse();
-    $status_messages = array('#theme' => 'status_messages');
-    return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
-  }
-
-  // Get the current element and count the number of files.
-  $current_element = $form;
-  foreach ($form_parents as $parent) {
-    $current_element = $current_element[$parent];
-  }
-  $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
-
-  // Process user input. $form and $form_state are modified in the process.
-  drupal_process_form($form['#form_id'], $form, $form_state);
-
-  // Retrieve the element to be rendered.
-  foreach ($form_parents as $parent) {
-    $form = $form[$parent];
-  }
-
-  // Add the special Ajax class if a new file was added.
-  if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
-    $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
-  }
-  // Otherwise just add the new content class on a placeholder.
-  else {
-    $form['#suffix'] .= '<span class="ajax-new-content"></span>';
-  }
-
-  $status_messages = array('#theme' => 'status_messages');
-  $form['#prefix'] .= drupal_render($status_messages);
-  $output = drupal_render($form);
-  $js = drupal_add_js();
-  $settings = drupal_merge_js_settings($js['settings']['data']);
-
-  $response = new AjaxResponse();
-  return $response->addCommand(new ReplaceCommand(NULL, $output, $settings));
-}
-
-/**
- * Ajax callback: Retrieves upload progress.
- *
- * @param $key
- *   The unique key for this upload process.
- */
-function file_ajax_progress($key) {
-  $progress = array(
-    'message' => t('Starting upload...'),
-    'percentage' => -1,
-  );
-
-  $implementation = file_progress_implementation();
-  if ($implementation == 'uploadprogress') {
-    $status = uploadprogress_get_info($key);
-    if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
-      $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
-      $progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
-    }
-  }
-  elseif ($implementation == 'apc') {
-    $status = apc_fetch('upload_' . $key);
-    if (isset($status['current']) && !empty($status['total'])) {
-      $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
-      $progress['percentage'] = round(100 * $status['current'] / $status['total']);
-    }
-  }
-
-  return new JsonResponse($progress);
-}
-
 /**
  * Determines the preferred upload progress implementation.
  *
@@ -1239,7 +1139,13 @@ function file_managed_file_process($element, &$form_state, $form) {
   $element['#tree'] = TRUE;
 
   $ajax_settings = array(
-    'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
+    'path' => 'file/ajax',
+    'options' => array(
+      'query' => array(
+        'element_parents' => implode('/', $element['#array_parents']),
+        'form_build_id' => $form['form_build_id']['#value'],
+      ),
+    ),
     'wrapper' => $element['#id'] . '-ajax-wrapper',
     'effect' => 'fade',
     'progress' => array(
diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml
new file mode 100644
index 000000000000..c2a9efd33d70
--- /dev/null
+++ b/core/modules/file/file.routing.yml
@@ -0,0 +1,12 @@
+file.ajax.upload:
+  pattern: '/file/ajax'
+  defaults:
+    _controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
+  requirements:
+    _permission: 'access content'
+file.ajax.progress:
+  pattern: '/file/progress'
+  defaults:
+    _controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress'
+  requirements:
+    _permission: 'access content'
diff --git a/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php b/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php
new file mode 100644
index 000000000000..dde6bbe83be7
--- /dev/null
+++ b/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\file\FileWidgetAjaxController.
+ */
+
+namespace Drupal\file\Controller;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\system\Controller\FormAjaxController;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
+
+/**
+ * Defines a controller to respond to file widget AJAX requests.
+ */
+class FileWidgetAjaxController extends FormAjaxController {
+
+  /**
+   * Processes AJAX file uploads and deletions.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AjaxResponse object.
+   */
+  public function upload(Request $request) {
+    $form_parents = explode('/', $request->query->get('element_parents'));
+    $form_build_id = $request->query->get('form_build_id');
+    $request_form_build_id = $request->request->get('form_build_id');
+
+    if (empty($request_form_build_id) || $form_build_id !== $request_form_build_id) {
+      // Invalid request.
+      drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
+      $response = new AjaxResponse();
+      $status_messages = array('#theme' => 'status_messages');
+      return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
+    }
+
+    try {
+      list($form, $form_state) = $this->getForm($request);
+    }
+    catch (HttpExceptionInterface $e) {
+      // Invalid form_build_id.
+      drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
+      $response = new AjaxResponse();
+      $status_messages = array('#theme' => 'status_messages');
+      return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
+    }
+
+    // Get the current element and count the number of files.
+    $current_element = NestedArray::getValue($form, $form_parents);
+    $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
+
+    // Process user input. $form and $form_state are modified in the process.
+    drupal_process_form($form['#form_id'], $form, $form_state);
+
+    // Retrieve the element to be rendered.
+    $form = NestedArray::getValue($form, $form_parents);
+
+    // Add the special Ajax class if a new file was added.
+    if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
+      $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+    }
+    // Otherwise just add the new content class on a placeholder.
+    else {
+      $form['#suffix'] .= '<span class="ajax-new-content"></span>';
+    }
+
+    $status_messages = array('#theme' => 'status_messages');
+    $form['#prefix'] .= drupal_render($status_messages);
+    $output = drupal_render($form);
+    $js = drupal_add_js();
+    $settings = drupal_merge_js_settings($js['settings']['data']);
+
+    $response = new AjaxResponse();
+    return $response->addCommand(new ReplaceCommand(NULL, $output, $settings));
+  }
+
+  /**
+   * Returns the progress status for a file upload process.
+   *
+   * @param string $key
+   *   The unique key for this upload process.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   A JsonResponse object.
+   */
+  public function progress($key) {
+    $progress = array(
+      'message' => t('Starting upload...'),
+      'percentage' => -1,
+    );
+
+    $implementation = file_progress_implementation();
+    if ($implementation == 'uploadprogress') {
+      $status = uploadprogress_get_info($key);
+      if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
+        $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
+        $progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
+      }
+    }
+    elseif ($implementation == 'apc') {
+      $status = apc_fetch('upload_' . $key);
+      if (isset($status['current']) && !empty($status['total'])) {
+        $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
+        $progress['percentage'] = round(100 * $status['current'] / $status['total']);
+      }
+    }
+
+    return new JsonResponse($progress);
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php b/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php
new file mode 100644
index 000000000000..f1645a7dc6a1
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\FormAjaxController.
+ */
+
+namespace Drupal\system\Controller;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Defines a controller to respond to form Ajax requests.
+ */
+class FormAjaxController {
+
+  /**
+   * Processes an Ajax form submission.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object.
+   *
+   * @return mixed
+   *   Whatever is returned by the triggering element's #ajax['callback']
+   *   function. One of:
+   *   - A render array containing the new or updated content to return to the
+   *     browser. This is commonly an element within the rebuilt form.
+   *   - A \Drupal\Core\Ajax\AjaxResponse object containing commands for the
+   *     browser to process.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
+   */
+  public function content(Request $request) {
+    list($form, $form_state) = $this->getForm($request);
+    drupal_process_form($form['#form_id'], $form, $form_state);
+
+    // We need to return the part of the form (or some other content) that needs
+    // to be re-rendered so the browser can update the page with changed content.
+    // Since this is the generic menu callback used by many Ajax elements, it is
+    // up to the #ajax['callback'] function of the element (may or may not be a
+    // button) that triggered the Ajax request to determine what needs to be
+    // rendered.
+    if (!empty($form_state['triggering_element'])) {
+      $callback = $form_state['triggering_element']['#ajax']['callback'];
+    }
+    if (empty($callback) || !is_callable($callback)) {
+      throw new HttpException(500, t('Internal Server Error'));
+    }
+    return call_user_func_array($callback, array(&$form, &$form_state));
+  }
+
+  /**
+   * Gets a form submitted via #ajax during an Ajax callback.
+   *
+   * This will load a form from the form cache used during Ajax operations. It
+   * pulls the form info from the request body.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object.
+   *
+   * @return array
+   *   An array containing the $form and $form_state. Use the list() function
+   *   to break these apart:
+   *   @code
+   *     list($form, $form_state, $form_id, $form_build_id) = $this->getForm();
+   *   @endcode
+   *
+   * @throws Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
+   */
+  protected function getForm(Request $request) {
+    $form_state = form_state_defaults();
+    $form_build_id = $request->request->get('form_build_id');
+
+    // Get the form from the cache.
+    $form = form_get_cache($form_build_id, $form_state);
+    if (!$form) {
+      // If $form cannot be loaded from the cache, the form_build_id must be
+      // invalid, which means that someone performed a POST request onto
+      // system/ajax without actually viewing the concerned form in the browser.
+      // This is likely a hacking attempt as it never happens under normal
+      // circumstances.
+      watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
+      throw new BadRequestHttpException();
+    }
+
+    // Since some of the submit handlers are run, redirects need to be disabled.
+    $form_state['no_redirect'] = TRUE;
+
+    // When a form is rebuilt after Ajax processing, its #build_id and #action
+    // should not change.
+    // @see drupal_rebuild_form()
+    $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
+    $form_state['rebuild_info']['copy']['#action'] = TRUE;
+
+    // The form needs to be processed; prepare for that by setting a few internal
+    // variables.
+    $form_state['input'] = $request->request->all();
+    $form_id = $form['#form_id'];
+
+    return array($form, $form_state, $form_id, $form_build_id);
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
index b464a46fb842..9b1781f00f23 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
@@ -51,5 +51,15 @@ function testSimpleAjaxFormValue() {
       $expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
       $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
     }
+
+    // Verify that AJAX elements with invalid callbacks return error code 500.
+    foreach (array('null', 'empty', 'nonexistent') as $key) {
+      $element_name = 'select_' . $key . '_callback';
+      $edit = array(
+        $element_name => 'red',
+      );
+      $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, $element_name);
+      $this->assertResponse(500);
+    }
   }
 }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 5b48ef2b6047..59c9bfa74598 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -619,12 +619,9 @@ function system_menu() {
   );
   $items['system/ajax'] = array(
     'title' => 'AHAH callback',
-    'page callback' => 'ajax_form_callback',
-    'access callback' => TRUE,
+    'route_name' => 'system.ajax',
     'theme callback' => 'ajax_base_page_theme',
     'type' => MENU_CALLBACK,
-    'file path' => 'core/includes',
-    'file' => 'form.inc',
   );
   $items['admin'] = array(
     'title' => 'Administration',
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 1adf224f8f8f..61363d6cda3e 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -1,3 +1,9 @@
+system.ajax:
+  pattern: '/system/ajax'
+  defaults:
+    _controller: '\Drupal\system\Controller\FormAjaxController::content'
+  requirements:
+    _access: 'TRUE'
 system.cron:
   pattern: '/cron/{key}'
   defaults:
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
index 1682c0181448..67f53225830d 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
@@ -72,6 +72,22 @@ function ajax_forms_test_simple_form($form, &$form_state) {
     '#type' => 'submit',
     '#value' => t('submit'),
   );
+
+  // This is for testing invalid callbacks that should return a 500 error in
+  // \Drupal\system\FormAjaxController::content().
+  $invalid_callbacks = array(
+    'null' => NULL,
+    'empty' => '',
+    'nonexistent' => 'some_function_that_does_not_exist',
+  );
+  foreach ($invalid_callbacks as $key => $value) {
+    $form['select_' . $key . '_callback'] = array(
+      '#type' => 'select',
+      '#options' => array('red' => 'red'),
+      '#ajax' => array('callback' => $value),
+    );
+  }
+
   return $form;
 }
 
-- 
GitLab