diff --git a/core/lib/Drupal/Core/EventSubscriber/FormAjaxSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FormAjaxSubscriber.php index 9bea727..2f10295 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FormAjaxSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FormAjaxSubscriber.php @@ -7,7 +7,6 @@ namespace Drupal\Core\EventSubscriber; -use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\Form\FormAjaxException; use Drupal\Core\Form\FormAjaxResponseBuilderInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -53,14 +52,11 @@ public function onException(GetResponseForExceptionEvent $event) { $request = $event->getRequest(); $form = $exception->getForm(); $form_state = $exception->getFormState(); - $form_build_id = $request->get('form_build_id'); - $commands = []; - if ($form_build_id !== $form['#build_id']) { - // If the form build ID has changed, issue an Ajax command to update it. - $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']); - } - $response = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands); + // Set the build ID from the request as the old build ID on the form. + $form['#build_id_old'] = $request->get('form_build_id'); + + $response = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, []); // Since this response is being set in place of an exception, explicitly // mark this as a 200 status. $response->headers->set('X-Status-Code', 200); diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 1289596..cc13ae4 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -12,7 +12,6 @@ use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Url; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -244,9 +243,6 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f 'callback' => array(get_class($this), 'addMoreAjax'), 'wrapper' => $wrapper_id, 'effect' => 'fade', - // @todo Remove this. - 'url' => Url::fromRoute('system.ajax'), - 'cache_form' => TRUE, ), ); } diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php index c8b2940..4b826ff 100644 --- a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php +++ b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Form; use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\Render\MainContent\MainContentRendererInterface; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\HttpFoundation\Request; @@ -52,6 +53,11 @@ public function __construct(MainContentRendererInterface $ajax_renderer, RouteMa * {@inheritdoc} */ public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands) { + // If the form build ID has changed, issue an Ajax command to update it. + if (isset($form['#build_id_old']) && $form['#build_id_old'] !== $form['#build_id']) { + $commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']); + } + // 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 diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index 5f8b452..123c743 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -69,6 +69,14 @@ public function getForm($form_arg); * The rendered form. This function may also perform a redirect and hence * may not return at all depending upon the $form_state flags that were set. * + * @throws \Drupal\Core\Form\FormAjaxException + * Thrown when a form is triggered via an AJAX submission. It will be + * handled by \Drupal\Core\EventSubscriber\FormAjaxSubscriber. + * @throws \Drupal\Core\Form\EnforcedResponseException + * Thrown when a form builder returns an exception directly, usually a + * \Symfony\Component\HttpFoundation\RedirectResponse. It will be handled by + * \Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber. + * * @see self::redirectForm() */ public function buildForm($form_id, FormStateInterface &$form_state); diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 0df954e..c5a2ae3 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -129,7 +129,6 @@ public static function preRenderGroup($element) { */ public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) { $element = static::preRenderAjaxForm($element); - // @todo Remove this. if (!empty($element['#ajax']['cache_form'])) { $form_state->setCached(); } diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php index 9719ea9..d6f7419 100644 --- a/core/modules/file/src/Element/ManagedFile.php +++ b/core/modules/file/src/Element/ManagedFile.php @@ -146,7 +146,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st $ajax_settings = [ 'url' => Url::fromRoute('file.ajax_upload'), - // @todo Remove this. + // @todo Remove this in https://www.drupal.org/node/2500527. 'cache_form' => TRUE, 'options' => [ 'query' => [ diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index f296702..721fdd9 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -1504,8 +1504,6 @@ protected function drupalGetAjax($path, array $options = array(), array $headers * is done by drupalPostAjaxForm(). This string is literally appended to the * POST data, so it must already be urlencoded and contain a leading "&" * (e.g., "&extra_var1=hello+world&extra_var2=you%26me"). - * - * @return bool|string|null */ protected function drupalPostForm($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { $submit_matches = FALSE; @@ -2624,11 +2622,7 @@ protected function prepareRequestForGenerator($clean_urls = TRUE, $override_serv */ protected function buildUrl($path, array $options = array()) { if ($path instanceof Url) { - $absolute_path = clone $path; - return $absolute_path - ->setOptions($options) - ->setAbsolute() - ->toString(); + return $path->setAbsolute()->toString(); } // The URL generator service is not necessarily available yet; e.g., in // interactive installer tests. diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php index 9f6ca98..edef325 100644 --- a/core/modules/system/src/Controller/FormAjaxController.php +++ b/core/modules/system/src/Controller/FormAjaxController.php @@ -7,7 +7,6 @@ namespace Drupal\system\Controller; -use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Form\FormAjaxResponseBuilderInterface; use Drupal\Core\Form\FormState; @@ -129,17 +128,6 @@ protected function getForm(Request $request) { throw new BadRequestHttpException(); } - // When a page level cache is enabled, the form-build id might have been - // replaced from within \Drupal::formBuilder()->getCache(). If this is the - // case, it is also necessary to update it in the browser by issuing an - // appropriate Ajax command. - $commands = []; - if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) { - // If the form build ID has changed, issue an Ajax command to update it. - $commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']); - $form_build_id = $form['#build_id']; - } - // Since some of the submit handlers are run, redirects need to be disabled. $form_state->disableRedirect(); @@ -156,7 +144,7 @@ protected function getForm(Request $request) { $form_state->setUserInput($request->request->all()); $form_id = $form['#form_id']; - return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands); + return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], $commands); } } diff --git a/core/modules/system/src/Tests/Ajax/MultiFormTest.php b/core/modules/system/src/Tests/Ajax/MultiFormTest.php index 449861c..860deca 100644 --- a/core/modules/system/src/Tests/Ajax/MultiFormTest.php +++ b/core/modules/system/src/Tests/Ajax/MultiFormTest.php @@ -81,7 +81,7 @@ function testMultiForm() { // page update, ensure the same as above. foreach ($field_xpaths as $form_html_id => $field_xpath) { for ($i = 0; $i < 2; $i++) { - $this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_html_id); + $this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), NULL, array(), array(), $form_html_id); $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, 'Found the correct number of field items after an AJAX submission.'); $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button after an AJAX submission.'); $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other'); diff --git a/core/modules/system/src/Tests/Form/RebuildTest.php b/core/modules/system/src/Tests/Form/RebuildTest.php index e993b49..53de2f2 100644 --- a/core/modules/system/src/Tests/Form/RebuildTest.php +++ b/core/modules/system/src/Tests/Form/RebuildTest.php @@ -96,7 +96,7 @@ function testPreserveFormActionAfterAJAX() { // submission and verify it worked by ensuring the updated page has two text // field items in the field for which we just added an item. $this->drupalGet('node/add/page'); - $this->drupalPostAjaxForm(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'node-page-form'); + $this->drupalPostAjaxForm(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), NULL, array(), array(), 'node-page-form'); $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.'); // Submit the form with the non-Ajax "Save" button, leaving the title field @@ -113,6 +113,9 @@ function testPreserveFormActionAfterAJAX() { // Ensure that the form's action is correct. $forms = $this->xpath('//form[contains(@class, "node-page-form")]'); - $this->assert(count($forms) == 1 && $forms[0]['action'] == Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), 'Re-rendered form contains the correct action value.'); + $this->assertEqual(1, count($forms)); + // Strip query params off the action before asserting. + $url = parse_url($forms[0]['action'])['path']; + $this->assertEqual(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url); } } 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 73d5b90..5289bcd 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 @@ -71,9 +71,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#options' => array('red' => 'red'), '#ajax' => array( 'callback' => $value, - // @todo Remove this. - 'url' => Url::fromRoute('system.ajax'), - 'cache_form' => TRUE, + // If the callback is NULL, the form will not post to the original URL + // and will expect the form to be cached. + 'cache_form' => $value === NULL, ), ); } diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc index b5cbda5..9cc4af8 100644 --- a/core/modules/views_ui/admin.inc +++ b/core/modules/views_ui/admin.inc @@ -53,7 +53,7 @@ function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_pa $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form'; // Specify the #ajax URL in order to retain form caching. - // @todo Remove this. + // @todo Remove this in https://www.drupal.org/node/2500523. $triggering_element['#ajax']['url'] = Url::fromRoute('system.ajax'); $triggering_element['#ajax']['cache_form'] = TRUE; diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FormAjaxSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FormAjaxSubscriberTest.php index 14ce121..21a1188 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/FormAjaxSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FormAjaxSubscriberTest.php @@ -7,7 +7,6 @@ namespace Drupal\Tests\Core\EventSubscriber; -use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\EventSubscriber\FormAjaxSubscriber; use Drupal\Core\Form\FormAjaxException; use Drupal\Core\Form\FormState; @@ -54,6 +53,9 @@ protected function setUp() { */ public function testOnException() { $form = ['#type' => 'form', '#build_id' => 'the_build_id']; + $expected_form = $form + [ + '#build_id_old' => 'the_build_id', + ]; $form_state = new FormState(); $exception = new FormAjaxException($form, $form_state); @@ -63,7 +65,7 @@ public function testOnException() { $this->formAjaxResponseBuilder->expects($this->once()) ->method('buildResponse') - ->with($request, $form, $form_state, $commands) + ->with($request, $expected_form, $form_state, $commands) ->willReturn($response); $event = new GetResponseForExceptionEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception); @@ -78,17 +80,19 @@ public function testOnException() { */ public function testOnExceptionNewBuildId() { $form = ['#type' => 'form', '#build_id' => 'the_build_id']; + $expected_form = $form + [ + '#build_id_old' => 'a_new_build_id', + ]; $form_state = new FormState(); $exception = new FormAjaxException($form, $form_state); $request = new Request([], ['form_build_id' => 'a_new_build_id']); $commands = []; - $commands[] = new UpdateBuildIdCommand('a_new_build_id', 'the_build_id'); $response = new Response(''); $this->formAjaxResponseBuilder->expects($this->once()) ->method('buildResponse') - ->with($request, $form, $form_state, $commands) + ->with($request, $expected_form, $form_state, $commands) ->willReturn($response); diff --git a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php index ba09680..0698e28 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php @@ -173,4 +173,40 @@ public function testBuildResponseWithCommands() { $this->assertSame($commands_expected, $result->getCommands()); } + /** + * @covers ::buildResponse + */ + public function testBuildResponseWithUpdateCommand() { + $triggering_element = [ + '#ajax' => [ + 'callback' => function (array $form, FormStateInterface $form_state) { + return new AjaxResponse([]); + } + ], + ]; + $request = new Request(); + $form = [ + '#build_id' => 'the_build_id', + '#build_id_old' => 'a_new_build_id', + 'test' => [ + '#type' => 'textfield', + ], + ]; + $form_state = new FormState(); + $form_state->setTriggeringElement($triggering_element); + $commands = [ + new AlertCommand('alert!'), + ]; + $commands_expected = []; + $commands_expected[] = ['command' => 'update_build_id', 'old' => 'a_new_build_id', 'new' => 'the_build_id']; + $commands_expected[] = ['command' => 'alert', 'text' => 'alert!']; + + $this->renderer->expects($this->never()) + ->method('renderResponse'); + + $result = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands); + $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result); + $this->assertSame($commands_expected, $result->getCommands()); + } + }