diff --git a/core/core.api.php b/core/core.api.php
index a8b13ca..e2ff4dd 100644
--- a/core/core.api.php
+++ b/core/core.api.php
@@ -2054,12 +2054,6 @@ function hook_validation_constraint_alter(array &$definitions) {
  * an array. Here are the details of its elements, all of which are optional:
  * - callback: The callback to invoke to handle the server side of the
  *   Ajax event. More information on callbacks is below in @ref sub_callback.
- * - path: The URL path to use for the request. If omitted, defaults to
- *   'system/ajax', which invokes the default Drupal Ajax processing (this will
- *   call the callback supplied in the 'callback' element). If you supply a
- *   path, you must set up a routing entry to handle the request yourself and
- *   return output described in @ref sub_callback below. See the
- *   @link menu Routing topic @endlink for more information on routing.
  * - wrapper: The HTML 'id' attribute of the area where the content returned by
  *   the callback should be placed. Note that callbacks have a choice of
  *   returning content or JavaScript commands; 'wrapper' is used for content
diff --git a/core/includes/pager.inc b/core/includes/pager.inc
index 7ce3407..0dab506 100644
--- a/core/includes/pager.inc
+++ b/core/includes/pager.inc
@@ -148,7 +148,12 @@ function pager_default_initialize($total, $limit, $element = 0) {
 function pager_get_query_parameters() {
   $query = &drupal_static(__FUNCTION__);
   if (!isset($query)) {
-    $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page'));
+    // There is a chance that the pager is rendered in the context of an AJAX
+    // form request. Therefore exclude AJAX_FORM_REQUEST from from the query
+    // arguments, as otherwise clicking the link would be interpreted
+    // potentially as an AJAX form request, especially when done as POST request
+    // using JS.
+    $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page', \Drupal\Core\Form\FormBuilderInterface::AJAX_FORM_REQUEST));
   }
   return $query;
 }
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index d966801..988779f 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -313,8 +313,11 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
    */
   public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) {
     $form = $this->retrieveForm($form_id, $form_state);
-    // All rebuilt forms will be cached.
-    $form_state->setCached();
+
+    // We don't allow to set state on GET requests.
+    if (!$form_state->isMethodType('GET')) {
+      $form_state->setCached();
+    }
 
     // If only parts of the form will be returned to the browser (e.g., Ajax or
     // RIA clients), or if the form already had a new build ID regenerated when
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 60ad704..bbfed36 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -108,9 +108,7 @@ public function buildForm($form_id, FormStateInterface &$form_state);
    * form 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
-   * Drupal\system\FormAjaxController::content() for more information about
-   * creating Ajax-enabled forms.
+   * one common use-case during which form rebuilding occurs.
    *
    * @param string $form_id
    *   The unique string identifying the desired form. If a function with that
@@ -130,7 +128,6 @@ public function buildForm($form_id, FormStateInterface &$form_state);
    *   The newly built form.
    *
    * @see self::processForm()
-   * @see \Drupal\system\FormAjaxController::content()
    */
   public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL);
 
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 4a17d6b..e26569e 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -141,16 +141,16 @@ class FormState implements FormStateInterface {
   /**
    * The HTTP form method to use for finding the input for this form.
    *
-   * May be 'post' or 'get'. Defaults to 'post'. Note that 'get' method forms do
+   * May be 'POST' or 'GET'. Defaults to 'POST'. Note that 'GET' method forms do
    * not use form ids so are always considered to be submitted, which can have
-   * unexpected effects. The 'get' method should only be used on forms that do
-   * not change data, as that is exclusively the domain of 'post.'
+   * unexpected effects. The 'GET' method should only be used on forms that do
+   * not change data, as that is exclusively the domain of 'POST.'
    *
    * This property is uncacheable.
    *
    * @var string
    */
-  protected $method = 'post';
+  protected $method = 'POST';
 
   /**
    * If set to TRUE the original, unprocessed form structure will be cached,
@@ -475,6 +475,9 @@ public function getButtons() {
    * {@inheritdoc}
    */
   public function setCached($cache = TRUE) {
+    if ($this->isMethodType('GET')) {
+      throw new \LogicException('Caching on GET requests is not allowed.');
+    }
     $this->cache = (bool) $cache;
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index a3b362c..5579483 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
@@ -128,14 +129,7 @@ public static function preRenderGroup($element) {
    * @see self::preRenderAjaxForm()
    */
   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
-    $element = static::preRenderAjaxForm($element);
-
-    // If the element was processed as an #ajax element, and a custom URL was
-    // provided, set the form to be cached.
-    if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) {
-      $form_state->setCached();
-    }
-    return $element;
+    return static::preRenderAjaxForm($element);
   }
 
   /**
@@ -149,7 +143,6 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state
    *   Properties used:
    *   - #ajax['event']
    *   - #ajax['prevent']
-   *   - #ajax['url']
    *   - #ajax['callback']
    *   - #ajax['options']
    *   - #ajax['wrapper']
@@ -159,6 +152,9 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state
    *
    * @return array
    *   The processed element with the necessary JavaScript attached to it.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when an element provides a custom #ajax URL.
    */
   public static function preRenderAjaxForm($element) {
     // Skip already processed elements.
@@ -173,6 +169,12 @@ public static function preRenderAjaxForm($element) {
       return $element;
     }
 
+    // If #ajax is TRUE, convert it to an empty array. Depending on the #type,
+    // it will still be given an event.
+    if ($element['#ajax'] === TRUE) {
+      $element['#ajax'] = [];
+    }
+
     // Add a reasonable default event handler if none was specified.
     if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
       switch ($element['#type']) {
@@ -230,31 +232,28 @@ public static function preRenderAjaxForm($element) {
       $element['#attached']['library'][] = 'core/drupal.ajax';
 
       $settings = $element['#ajax'];
+      // Do not support custom URLs, $settings['url'] is generated below.
+      if (isset($settings['url'])) {
+        throw new \InvalidArgumentException('Custom AJAX URLs are not allowed');
+      }
 
-      // Assign default settings. When 'url' is set to NULL, ajax.js submits the
-      // Ajax request to the same URL as the form or link destination is for
-      // someone with JavaScript disabled. This is generally preferred as a way to
-      // ensure consistent server processing for js and no-js users, and Drupal's
-      // content negotiation takes care of formatting the response appropriately.
-      // However, 'url' and 'options' may be set when wanting server processing
-      // to be substantially different for a JavaScript triggered submission.
-      // One such substantial difference is form elements that use
-      // #ajax['callback'] for determining which part of the form needs
-      // re-rendering. For that, we have a special 'system.ajax' route which
-      // must be manually set.
+      // Assign default settings.
       $settings += [
-        'url' => NULL,
-        'options' => ['query' => []],
         'dialogType' => 'ajax',
       ];
-      if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
-        $settings['url'] = Url::fromRoute('<current>');
+      if (array_key_exists('callback', $settings)) {
         // Add all the current query parameters in order to ensure that we build
         // the same form on the AJAX POST requests. For example,
         // \Drupal\user\AccountForm takes query parameters into account in order
         // to hide the password field dynamically.
-        $settings['options']['query'] += \Drupal::request()->query->all();
-        $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+        $query = \Drupal::request()->query->all();
+        $query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+
+        // Add options['query'] => [] as default value.
+        $settings = NestedArray::mergeDeep($settings, ['options' => ['query' => []]]);
+        $options['query'] = $settings['options']['query'] + $query;
+
+        $settings['url'] = Url::fromRoute('<current>', [], $options)->toString();
       }
 
       // @todo Legacy support. Remove in Drupal 8.
@@ -262,13 +261,6 @@ public static function preRenderAjaxForm($element) {
         $settings['method'] = 'replaceWith';
       }
 
-      // Convert \Drupal\Core\Url object to string.
-      if (isset($settings['url']) && $settings['url'] instanceof Url) {
-        $settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
-      }
-      else {
-        $settings['url'] = NULL;
-      }
       unset($settings['options']);
 
       // Add special data to $settings['submit'] so that when this element
diff --git a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
index b15ce93..9d904ee 100644
--- a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
+++ b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
@@ -15,12 +15,12 @@
 /**
  * Defines a theme negotiator that deals with the active theme on ajax requests.
  *
- * Many different pages can invoke an Ajax request to system/ajax or another
- * generic Ajax path. It is almost always desired for an Ajax response to be
- * rendered using the same theme as the base page, because most themes are built
- * with the assumption that they control the entire page, so if the CSS for two
- * themes are both loaded for a given page, they may conflict with each other.
- * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
+ * Many different pages can invoke an Ajax request to a generic Ajax path. It is
+ * almost always desired for an Ajax response to be rendered using the same
+ * theme as the base page, because most themes are built with the assumption
+ * that they control the entire page, so if the CSS for two themes are both
+ * loaded for a given page, they may conflict with each other. For example,
+ * Bartik is Drupal's default theme, and Seven is Drupal's default
  * administration theme. Depending on whether the "Use the administration theme
  * when editing or creating content" checkbox is checked, the node edit form may
  * be displayed in either theme, but the Ajax response to the Field module's
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index 1c1b7e3..d05a4dd 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -7,13 +7,12 @@
 
 namespace Drupal\file\Controller;
 
-use Drupal\system\Controller\FormAjaxController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 
 /**
  * Defines a controller to respond to file widget AJAX requests.
  */
-class FileWidgetAjaxController extends FormAjaxController {
+class FileWidgetAjaxController {
 
   /**
    * Returns the progress status for a file upload process.
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index a006284..069a743 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -1593,15 +1593,14 @@ protected function drupalGetAjax($path, array $options = array(), array $headers
    *   case, this value needs to be an array with the following keys:
    *   - path: A path to submit the form values to for Ajax-specific processing,
    *     which is likely different than the $path parameter used for retrieving
-   *     the initial form. Defaults to 'system/ajax'.
-   *   - triggering_element: If the value for the 'path' key is 'system/ajax' or
-   *     another generic Ajax processing path, this needs to be set to the name
-   *     of the element. If the name doesn't identify the element uniquely, then
-   *     this should instead be an array with a single key/value pair,
-   *     corresponding to the element name and value. The callback for the
-   *     generic Ajax processing path uses this to find the #ajax information
-   *     for the element, including which specific callback to use for
-   *     processing the request.
+   *     the initial form.
+   *   - triggering_element: If the value for the 'path' key is a generic Ajax
+   *     processing path, this needs to be set to the name of the element. If
+   *     the name doesn't identify the element uniquely, then this should
+   *     instead be an array with a single key/value pair, corresponding to the
+   *     element name and value. The callback for the generic Ajax processing
+   *     path uses this to find the #ajax information for the element, including
+   *     which specific callback to use for processing the request.
    *
    *   This can also be set to NULL in order to emulate an Internet Explorer
    *   submission of a form with a single text field, and pressing ENTER in that
@@ -1649,7 +1648,10 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array(
         $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form);
         $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl();
         if ($ajax) {
-          $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax');
+          if (empty($submit['path'])) {
+            throw new \Exception('No #ajax path specified.');
+          }
+          $action = $this->getAbsoluteUrl($submit['path']);
           // Ajax callbacks verify the triggering element if necessary, so while
           // we may eventually want extra code that verifies it in the
           // handleForm() function, it's not currently a requirement.
@@ -1735,8 +1737,7 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array(
    *   and the value is the button label. i.e.) array('op' => t('Refresh')).
    * @param $ajax_path
    *   (optional) Override the path set by the Ajax settings of the triggering
-   *   element. In the absence of both the triggering element's Ajax path and
-   *   $ajax_path 'system/ajax' will be used.
+   *   element.
    * @param $options
    *   (optional) Options to be forwarded to the url generator.
    * @param $headers
@@ -1807,7 +1808,7 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
     $extra_post = '&' . $this->serializePostValues($extra_post);
 
     // Unless a particular path is specified, use the one specified by the
-    // Ajax settings, or else 'system/ajax'.
+    // Ajax settings.
     if (!isset($ajax_path)) {
       if (isset($ajax_settings['url'])) {
         // In order to allow to set for example the wrapper envelope query
@@ -1824,10 +1825,12 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
           $parsed_url['path']
         );
       }
-      else {
-        $ajax_path = 'system/ajax';
-      }
     }
+
+    if (empty($ajax_path)) {
+      throw new \Exception('No #ajax path specified.');
+    }
+
     $ajax_path = $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options);
 
     // Submit the POST request.
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
deleted file mode 100644
index 084847a..0000000
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\system\Controller\FormAjaxController.
- */
-
-namespace Drupal\system\Controller;
-
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
-use Drupal\Core\Form\FormState;
-use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\Core\Render\MainContent\MainContentRendererInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\system\FileAjaxForm;
-use Psr\Log\LoggerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-
-/**
- * Defines a controller to respond to form Ajax requests.
- */
-class FormAjaxController implements ContainerInjectionInterface {
-
-  /**
-   * A logger instance.
-   *
-   * @var \Psr\Log\LoggerInterface
-   */
-  protected $logger;
-
-  /**
-   * The form builder.
-   *
-   * @var \Drupal\Core\Form\FormBuilderInterface|\Drupal\Core\Form\FormCacheInterface
-   */
-  protected $formBuilder;
-
-  /**
-   * The renderer.
-   *
-   * @var \Drupal\Core\Render\RendererInterface
-   */
-  protected $renderer;
-
-  /**
-   * The main content to AJAX Response renderer.
-   *
-   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
-   */
-  protected $ajaxRenderer;
-
-  /**
-   * The current route match.
-   *
-   * @var \Drupal\Core\Routing\RouteMatchInterface
-   */
-  protected $routeMatch;
-
-  /**
-   * The form AJAX response builder.
-   *
-   * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
-   */
-  protected $formAjaxResponseBuilder;
-
-  /**
-   * Constructs a FormAjaxController object.
-   *
-   * @param \Psr\Log\LoggerInterface $logger
-   *   A logger instance.
-   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
-   *   The form builder.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer.
-   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
-   *   The main content to AJAX Response renderer.
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
-   *   The current route match.
-   * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
-   *   The form AJAX response builder.
-   */
-  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match, FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
-    $this->logger = $logger;
-    $this->formBuilder = $form_builder;
-    $this->renderer = $renderer;
-    $this->ajaxRenderer = $ajax_renderer;
-    $this->routeMatch = $route_match;
-    $this->formAjaxResponseBuilder = $form_ajax_response_builder;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('logger.factory')->get('ajax'),
-      $container->get('form_builder'),
-      $container->get('renderer'),
-      $container->get('main_content_renderer.ajax'),
-      $container->get('current_route_match'),
-      $container->get('form_ajax_response_builder')
-    );
-  }
-
-  /**
-   * 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) {
-    $ajax_form = $this->getForm($request);
-    $form = $ajax_form->getForm();
-    $form_state = $ajax_form->getFormState();
-    $commands = $ajax_form->getCommands();
-
-    $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
-
-    return $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $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 the request body.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request object.
-   *
-   * @return \Drupal\system\FileAjaxForm
-   *   A wrapper object containing the $form, $form_state, $form_id,
-   *   $form_build_id and an initial list of Ajax $commands.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
-   */
-  protected function getForm(Request $request) {
-    $form_state = new FormState();
-    $form_build_id = $request->request->get('form_build_id');
-
-    // Get the form from the cache.
-    $form = $this->formBuilder->getCache($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.
-      $this->logger->warning('Invalid form POST data.');
-      throw new BadRequestHttpException();
-    }
-
-    // Since some of the submit handlers are run, redirects need to be disabled.
-    $form_state->disableRedirect();
-
-    // When a form is rebuilt after Ajax processing, its #build_id and #action
-    // should not change.
-    // @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm()
-    $form_state->addRebuildInfo('copy', [
-      '#build_id' => TRUE,
-      '#action' => TRUE,
-    ]);
-
-    // The form needs to be processed; prepare for that by setting a few
-    // internal variables.
-    $form_state->setUserInput($request->request->all());
-    $form_id = $form['#form_id'];
-
-    return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], []);
-  }
-
-}
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
index f151c33..4ebeed6 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
+++ b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
@@ -37,15 +37,6 @@ public function testFormCacheUsage() {
 
     // The number of cache entries should not have changed.
     $this->assertEqual(0, count($key_value_expirable->getAll()));
-
-    // Visit a form that is explicitly cached, 3 times.
-    $cached_form_url = Url::fromRoute('ajax_forms_test.cached_form');
-    $this->drupalGet($cached_form_url);
-    $this->drupalGet($cached_form_url);
-    $this->drupalGet($cached_form_url);
-
-    // The number of cache entries should be exactly 3.
-    $this->assertEqual(3, count($key_value_expirable->getAll()));
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
index 03727b6..136ec69 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
+++ b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
@@ -35,7 +35,7 @@ protected function getFormBuildId() {
   }
 
   /**
-   * Create a simple form, then POST to system/ajax to change to it.
+   * Create a simple form, then POST via AJAX to change to it.
    */
   public function testSimpleAJAXFormValue() {
    $this->drupalGet('ajax_forms_test_get_form');
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 12e7d9a..a386196 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -1,12 +1,3 @@
-system.ajax:
-  path: '/system/ajax'
-  defaults:
-    _controller: '\Drupal\system\Controller\FormAjaxController::content'
-  options:
-    _theme: ajax_base_page
-  requirements:
-    _access: 'TRUE'
-
 system.401:
   path: '/system/401'
   defaults:
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
index 3caf5ba..ccca279 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
@@ -30,10 +30,3 @@ ajax_forms_test.lazy_load_form:
   requirements:
     _access: 'TRUE'
 
-ajax_forms_test.cached_form:
-  path: '/ajax_forms_test_cached_form'
-  defaults:
-    _title: 'AJAX forms cached form test'
-    _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestCachedForm'
-  requirements:
-    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php
deleted file mode 100644
index 3b22783..0000000
--- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\ajax_forms_test\Form\AjaxFormsTestCachedForm.
- */
-
-namespace Drupal\ajax_forms_test\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-
-/**
- * Provides an AJAX form that will be cached.
- */
-class AjaxFormsTestCachedForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'ajax_form_cache_test_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['test1'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Test 1'),
-      '#options' => [
-        'option1' => $this->t('Option 1'),
-        'option2' => $this->t('Option 2'),
-      ],
-      '#ajax' => [
-        'url' => Url::fromRoute('system.ajax'),
-      ],
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-  }
-
-}
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php
index 225664d..2fcfd1f 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestSimpleForm.php
@@ -57,7 +57,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     );
 
     // This is for testing invalid callbacks that should return a 500 error in
-    // \Drupal\system\FormAjaxController::content().
+    // \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse().
     $invalid_callbacks = array(
       'null' => NULL,
       'empty' => '',
diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php
index 386456c..4368efe 100644
--- a/core/modules/views/src/Plugin/views/field/Field.php
+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -447,9 +447,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
       '#title' => $this->t('Formatter'),
       '#options' => $formatters,
       '#default_value' => $this->options['type'],
-      '#ajax' => array(
-        'url' => views_ui_build_form_url($form_state),
-      ),
+      '#ajax' => TRUE,
       '#submit' => array(array($this, 'submitTemporaryForm')),
       '#executes_submit_callback' => TRUE,
     );
diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php
index e290789..84901d0 100644
--- a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php
+++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php
@@ -181,9 +181,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $
         '#value' => $this->t('Remove'),
         '#submit' => array(array($this, 'remove')),
         '#limit_validation_errors' => array(array('override')),
-        '#ajax' => array(
-          'url' => Url::fromRoute('<current>'),
-        ),
+        '#ajax' => TRUE,
         '#button_type' => 'danger',
       );
     }
diff --git a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php
index 06bbd0a..fc43a28 100644
--- a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php
+++ b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php
@@ -134,7 +134,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
             'class' => array('views-remove-group'),
           ),
           '#group' => $id,
-          '#ajax' => ['url' => NULL],
+          '#ajax' => TRUE,
         );
       }
       $group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', array('@group' => $id));
@@ -218,7 +218,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#attributes' => array(
         'class' => array('views-add-group'),
       ),
-      '#ajax' => ['url' => NULL],
+      '#ajax' => TRUE,
     );
 
     return $form;
diff --git a/core/modules/views_ui/src/Tests/PreviewTest.php b/core/modules/views_ui/src/Tests/PreviewTest.php
index ff10213..4771b51 100644
--- a/core/modules/views_ui/src/Tests/PreviewTest.php
+++ b/core/modules/views_ui/src/Tests/PreviewTest.php
@@ -278,7 +278,7 @@ protected function clickPreviewLinkAJAX($url, $row_count) {
    * @param int $row_count
    *   The expected number of rows in the preview.
    */
-  protected function assertPreviewAJAX($result, $row_count) {
+  protected function assertPreviewAJAX(array $result, $row_count) {
     // Has AJAX callback replied with an insert command? If so, we can
     // assume that the page content was updated with AJAX returned data.
     $result_commands = array();
diff --git a/core/modules/views_ui/src/ViewPreviewForm.php b/core/modules/views_ui/src/ViewPreviewForm.php
index 7ec8f4d..fce89dc 100644
--- a/core/modules/views_ui/src/ViewPreviewForm.php
+++ b/core/modules/views_ui/src/ViewPreviewForm.php
@@ -76,7 +76,6 @@ public function form(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   protected function actions(array $form, FormStateInterface $form_state) {
-    $view = $this->entity;
     return array(
       '#attributes' => array(
         'id' => 'preview-submit-wrapper',
@@ -89,7 +88,7 @@ protected function actions(array $form, FormStateInterface $form_state) {
         '#submit' => array('::submitPreview'),
         '#id' => 'preview-submit',
         '#ajax' => array(
-          'url' => Url::fromRoute('entity.view.preview_form', ['view' => $view->id(), 'display_id' => $this->displayID]),
+          'callback' => [get_called_class(), 'previewSubmit'],
           'wrapper' => 'views-preview-wrapper',
           'event' => 'click',
           'progress' => array('type' => 'fullscreen'),
@@ -100,6 +99,19 @@ protected function actions(array $form, FormStateInterface $form_state) {
   }
 
   /**
+   * AJAX callback: Return the form as-is to let the views UI be rebuilt.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   *
+   * @return array
+   *   The form, with no changes.
+   */
+  public static function previewSubmit(array $form) {
+    return $form;
+  }
+
+  /**
    * Form submission handler for the Preview button.
    */
   public function submitPreview($form, FormStateInterface $form_state) {
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 05451be..2e3400e 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -12,7 +12,9 @@
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
 use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
+use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Url;
 use Drupal\views\Views;
 use Drupal\Core\Entity\EntityStorageInterface;
@@ -311,11 +313,6 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form
       $names = array(t('Apply'), t('Apply and continue'));
     }
 
-    // Views provides its own custom handling of AJAX form submissions. Usually
-    // this happens at the same path, but custom paths may be specified in
-    // $form_state.
-    $form_url = $form_state->get('url') ?: Url::fromRouteMatch(\Drupal::routeMatch());
-
     // Forms that are purely informational set an ok_button flag, so we know not
     // to create an "Apply" button for them.
     if (!$form_state->get('ok_button')) {
@@ -330,9 +327,7 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form
         // take care of running the regular submit handler as appropriate.
         '#submit' => array(array($this, 'standardSubmit')),
         '#button_type' => 'primary',
-        '#ajax' => array(
-          'url' => $form_url,
-        ),
+        '#ajax' => TRUE,
       );
       // Form API button click detection requires the button's #value to be the
       // same between the form build of the initial page request, and the
@@ -358,9 +353,7 @@ public function getStandardButtons(&$form, FormStateInterface $form_state, $form
       '#value' => !$form_state->get('ok_button') ? t('Cancel') : t('Ok'),
       '#submit' => array($cancel_submit),
       '#validate' => array(),
-      '#ajax' => array(
-        'path' => $form_url,
-      ),
+      '#ajax' => TRUE,
       '#limit_validation_errors' => array(),
     );
 
@@ -550,6 +543,10 @@ public function renderPreview($display_id, $args = array()) {
     $request_stack = \Drupal::requestStack();
     $current_request = $request_stack->getCurrentRequest();
     $executable = $this->getExecutable();
+    /** @var \Drupal\Core\Render\RendererInterface $renderer */
+    $renderer = \Drupal::service('renderer');
+    /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
+    $render_cache = \Drupal::service('render_cache');
 
     // Determine where the query and performance statistics should be output.
     $config = \Drupal::config('views.settings');
@@ -578,7 +575,7 @@ public function renderPreview($display_id, $args = array()) {
       // have some input in the query parameters, so we merge request() and
       // query() to ensure we get it all.
       $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all());
-      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
+      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', FormBuilderInterface::AJAX_FORM_REQUEST, AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
         if (isset($exposed_input[$key])) {
           unset($exposed_input[$key]);
         }
@@ -612,10 +609,13 @@ public function renderPreview($display_id, $args = array()) {
       $raw_parameters->set('display_id', $display_id);
       $request->attributes->set('_raw_variables', $raw_parameters);
 
+      $request->query->remove(FormBuilderInterface::AJAX_FORM_REQUEST);
+
       foreach ($args as $key => $arg) {
         $request->attributes->set('arg_' . $key, $arg);
       }
       $request_stack->push($request);
+      $executable->setRequest($request_stack->getCurrentRequest());
 
       // Suppress contextual links of entities within the result set during a
       // Preview.
@@ -855,6 +855,14 @@ public function renderPreview($display_id, $args = array()) {
       ];
     }
 
+    if (!empty($output['preview'])) {
+      // We render the preview now, in order to render it in the context of the
+      // previous setup request, because for example pager links depend on the
+      // "current" URL.
+      $renderer->renderPlain($output['preview']);
+      $output['preview'] = $render_cache->getCacheableRenderArray($output['preview']);
+    }
+
     // Ensure that we just remove an additional request we pushed earlier.
     // This could happen if $errors was not empty.
     if ($request_stack->getCurrentRequest() != $current_request) {
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 9e53645..c02555b 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -310,11 +310,51 @@ public function testRebuildForm() {
     $form_state->addRebuildInfo('copy', ['#build_id' => TRUE]);
     $this->formBuilder->processForm($form_id, $form, $form_state);
     $this->assertSame($original_build_id, $form['#build_id']);
+    $this->assertTrue($form_state->isCached());
 
     // Rebuild the form again, and assert that there is a new build ID.
     $form_state->setRebuildInfo([]);
     $form = $this->formBuilder->buildForm($form_arg, $form_state);
     $this->assertNotSame($original_build_id, $form['#build_id']);
+    $this->assertTrue($form_state->isCached());
+  }
+
+  /**
+   * Tests the rebuildForm() method for a GET submission.
+   */
+  public function testRebuildFormOnGetRequest() {
+    $form_id = 'test_form_id';
+    $expected_form = $form_id();
+
+    // The form will be built four times.
+    $form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
+    $form_arg->expects($this->exactly(2))
+      ->method('getFormId')
+      ->will($this->returnValue($form_id));
+    $form_arg->expects($this->exactly(4))
+      ->method('buildForm')
+      ->will($this->returnValue($expected_form));
+
+    // Do an initial build of the form and track the build ID.
+    $form_state = new FormState();
+    $form_state->setMethod('GET');
+    $form = $this->formBuilder->buildForm($form_arg, $form_state);
+    $original_build_id = $form['#build_id'];
+
+    // Rebuild the form, and assert that the build ID has not changed.
+    $form_state->setRebuild();
+    $input['form_id'] = $form_id;
+    $form_state->setUserInput($input);
+    $form_state->addRebuildInfo('copy', ['#build_id' => TRUE]);
+    $this->formBuilder->processForm($form_id, $form, $form_state);
+    $this->assertSame($original_build_id, $form['#build_id']);
+    $this->assertFalse($form_state->isCached());
+
+    // Rebuild the form again, and assert that there is a new build ID.
+    $form_state->setRebuildInfo([]);
+    $form = $this->formBuilder->buildForm($form_arg, $form_state);
+    $this->assertNotSame($original_build_id, $form['#build_id']);
+    $this->assertFalse($form_state->isCached());
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
index b92c5f7..8a44fc9 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
@@ -422,6 +422,11 @@ public function testIsCached($cache_key, $no_cache_key, $expected) {
       'cache' => $cache_key,
       'no_cache' => $no_cache_key,
     ]);
+
+    $form_state->setMethod('POST');
+    $this->assertSame($expected, $form_state->isCached());
+
+    $form_state->setMethod('GET');
     $this->assertSame($expected, $form_state->isCached());
   }
 
@@ -464,6 +469,28 @@ public function providerTestIsCached() {
   }
 
   /**
+   * @covers ::setCached
+   */
+  public function testSetCachedPost() {
+    $form_state = new FormState();
+    $form_state->setMethod('POST');
+    $form_state->setCached();
+    $this->assertTrue($form_state->isCached());
+  }
+
+  /**
+   * @covers ::setCached
+   *
+   * @expectedException \LogicException
+   * @expectedExceptionMessage Caching on GET requests is not allowed.
+   */
+  public function testSetCachedGet() {
+    $form_state = new FormState();
+    $form_state->setMethod('GET');
+    $form_state->setCached();
+  }
+
+  /**
    * @covers ::isMethodType
    * @covers ::setMethod
    *
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
new file mode 100644
index 0000000..99abc76
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\Element\RenderElementTest.
+ */
+
+namespace Drupal\Tests\Core\Render\Element;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Render\Element\RenderElement;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Element\RenderElement
+ * @group Render
+ */
+class RenderElementTest extends UnitTestCase {
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The container.
+   *
+   * @var \Drupal\Core\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->requestStack = new RequestStack();
+    $this->container = new ContainerBuilder();
+    $this->container->set('request_stack', $this->requestStack);
+    \Drupal::setContainer($this->container);
+  }
+
+  /**
+   * @covers ::preRenderAjaxForm
+   */
+  public function testPreRenderAjaxForm() {
+    $request = Request::create('/test');
+    $request->query->set('foo', 'bar');
+    $this->requestStack->push($request);
+
+    $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
+    $url = '/test?foo=bar&ajax_form=1';
+    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
+      ->willReturn($url);
+
+    $url_generator = $prophecy->reveal();
+    $this->container->set('url_generator', $url_generator);
+
+    $element = [
+      '#type' => 'select',
+      '#id' => 'test',
+      '#ajax' => [
+        'wrapper' => 'foo',
+        'callback' => 'test-callback',
+      ],
+    ];
+
+    $element = RenderElement::preRenderAjaxForm($element);
+
+    $this->assertTrue($element['#ajax_processed']);
+    $this->assertEquals($url, $element['#attached']['drupalSettings']['ajax']['test']['url']);
+  }
+
+  /**
+   * @covers ::preRenderAjaxForm
+   */
+  public function testPreRenderAjaxFormWithQueryOptions() {
+    $request = Request::create('/test');
+    $request->query->set('foo', 'bar');
+    $this->requestStack->push($request);
+
+    $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
+    $url = '/test?foo=bar&other=query&ajax_form=1';
+    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
+      ->willReturn($url);
+
+    $url_generator = $prophecy->reveal();
+    $this->container->set('url_generator', $url_generator);
+
+    $element = [
+      '#type' => 'select',
+      '#id' => 'test',
+      '#ajax' => [
+        'wrapper' => 'foo',
+        'callback' => 'test-callback',
+        'options' => [
+          'query' => [
+            'other' => 'query',
+          ],
+        ],
+      ],
+    ];
+
+    $element = RenderElement::preRenderAjaxForm($element);
+
+    $this->assertTrue($element['#ajax_processed']);
+    $this->assertEquals($url, $element['#attached']['drupalSettings']['ajax']['test']['url']);
+  }
+
+  /**
+   * @covers ::preRenderAjaxForm
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testPreRenderAjaxFormWithDisallowedUrl() {
+    $element = [
+      '#type' => 'select',
+      '#id' => 'test',
+      '#ajax' => [
+        'url' => '/foo',
+      ],
+    ];
+
+    $element = RenderElement::preRenderAjaxForm($element);
+
+    $this->assertTrue($element['#ajax_processed']);
+  }
+
+  /**
+   * @covers ::preRenderAjaxForm
+   */
+  public function testPreRenderAjaxFormAjaxTrue() {
+    $element = [
+      '#type' => 'select',
+      '#id' => 'test',
+      '#ajax' => TRUE,
+    ];
+
+    $element = RenderElement::preRenderAjaxForm($element);
+
+    $this->assertTrue($element['#ajax_processed']);
+    $ajax_settings = [
+      'event' => 'change',
+      'dialogType' => 'ajax',
+    ];
+    $this->assertSame($ajax_settings, $element['#attached']['drupalSettings']['ajax']['test']);
+  }
+
+}
