diff --git a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php index 174d1bb..35d6e85 100644 --- a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php +++ b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormAjaxResponseBuilderInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\KernelEvents; /** @@ -19,11 +20,6 @@ class FormAjaxSubscriber implements EventSubscriberInterface { /** - * Request key for a form that will post to original URL on AJAX submission. - */ - const POST_TO_ORIGINAL_URL = 'post_to_original_url'; - - /** * The form AJAX response builder. * * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface @@ -56,11 +52,18 @@ public function onException(GetResponseForExceptionEvent $event) { // 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); - $event->setResponse($response); + try { + $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); + $event->setResponse($response); + } + catch (HttpException $e) { + // Otherwise, replace the existing exception with the new one. + $event->setException($e); + } } } diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php index 8822a33..720ecc8 100644 --- a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php @@ -28,6 +28,9 @@ * * @return \Drupal\Core\Ajax\AjaxResponse * An AJAX response representing the form and its AJAX commands. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * Thrown if the AJAX callback is not a callable. */ public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands); diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index ea1e3ab..382bba1 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -14,7 +14,6 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\CsrfTokenGenerator; use Drupal\Core\DependencyInjection\ClassResolverInterface; -use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\ElementInfoManagerInterface; @@ -247,7 +246,7 @@ public function buildForm($form_id, FormStateInterface &$form_state) { // If this form should post to the original URL, disable all form redirects. $request = $this->requestStack->getCurrentRequest(); - if ($post_to_original_url = $request->query->has(FormAjaxSubscriber::POST_TO_ORIGINAL_URL)) { + if ($post_to_original_url = $request->query->has(static::POST_TO_ORIGINAL_URL)) { $form_state->disableRedirect(); } @@ -265,8 +264,12 @@ public function buildForm($form_id, FormStateInterface &$form_state) { $response = $this->processForm($form_id, $form, $form_state); // After processing the form, if this is to be posted to the original URL, - // interrupt form rendering and return. - // @see \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() + // interrupt form rendering and return by throwing an exception that + // contains the processed form and form state. This exception will be caught + // by \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() + // and then passed through + // \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse() to + // build a proper AJAX response. if ($post_to_original_url) { throw new FormAjaxException($form, $form_state); } diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index 53641c6..76d3342 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -13,6 +13,11 @@ interface FormBuilderInterface { /** + * Request key for a form that will post to original URL on AJAX submission. + */ + const POST_TO_ORIGINAL_URL = 'post_to_original_url'; + + /** * Determines the ID of a form. * * @param \Drupal\Core\Form\FormInterface|string $form_arg diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index f82420e..623f418 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Render\Element; -use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber; +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Render\Element; @@ -241,7 +241,7 @@ public static function preRenderAjaxForm($element) { // re-rendering. For that, we have a special 'system/ajax' route. if (isset($settings['callback']) && !isset($settings['url'])) { $settings['url'] = Url::fromRoute(''); - $settings['options']['query'][FormAjaxSubscriber::POST_TO_ORIGINAL_URL] = TRUE; + $settings['options']['query'][FormBuilderInterface::POST_TO_ORIGINAL_URL] = TRUE; } $settings += array( 'url' => NULL, diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php new file mode 100644 index 0000000..7d73c82 --- /dev/null +++ b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php @@ -0,0 +1,49 @@ +get('form'); + $this->drupalLogin($this->rootUser); + + // Ensure that the cache is empty. + $this->assertEqual(0, count($key_value_expirable->getAll())); + + // Visit an AJAX form that is not cached, 3 times. + $uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form'); + $this->drupalGet($uncached_form_url); + $this->drupalGet($uncached_form_url); + $this->drupalGet($uncached_form_url); + + // 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.get_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/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php index 6ac88a9..603cfa0 100644 --- a/core/modules/system/src/Tests/Ajax/DialogTest.php +++ b/core/modules/system/src/Tests/Ajax/DialogTest.php @@ -7,8 +7,8 @@ namespace Drupal\system\Tests\Ajax; -use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Url; /** @@ -166,7 +166,7 @@ public function testDialog() { 'edit-preview' => [ 'callback' => '::preview', 'event' => 'click', - 'url' => Url::fromRoute('ajax_test.dialog_form')->setOption('query', [FormAjaxSubscriber::POST_TO_ORIGINAL_URL => TRUE])->toString(), + 'url' => Url::fromRoute('ajax_test.dialog_form')->setOption('query', [FormBuilderInterface::POST_TO_ORIGINAL_URL => TRUE])->toString(), 'dialogType' => 'ajax', 'submit' => [ '_triggering_element_name' => 'op', diff --git a/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php index 9f93b41..19adec8 100644 --- a/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; /** @@ -95,7 +96,6 @@ public function testOnExceptionNewBuildId() { ->with($request, $expected_form, $form_state, $commands) ->willReturn($response); - $event = new GetResponseForExceptionEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception); $this->subscriber->onException($event); @@ -119,4 +119,30 @@ public function testOnExceptionOtherClass() { $this->assertNull($event->getResponse()); } + /** + * @covers ::onException + */ + public function testOnExceptionResponseBuilderException() { + $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); + $request = new Request([], ['form_build_id' => 'the_build_id']); + $commands = []; + + $expected_exception = new HttpException(500, 'The specified #ajax callback is empty or not callable.'); + $this->formAjaxResponseBuilder->expects($this->once()) + ->method('buildResponse') + ->with($request, $expected_form, $form_state, $commands) + ->willThrowException($expected_exception); + + $event = new GetResponseForExceptionEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception); + $this->subscriber->onException($event); + + $this->assertNull($event->getResponse()); + $this->assertSame($expected_exception, $event->getException()); + } + }