Commit dda70f71 authored by alexpott's avatar alexpott

Issue #2230121 by znerol, dawehner | sun: Fixed Remove exit() from FormBuilder.

parent 39aeee85
......@@ -173,7 +173,7 @@ services:
arguments: [default]
form_builder:
class: Drupal\Core\Form\FormBuilder
arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@theme.manager', '@?csrf_token', '@?kernel']
arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@theme.manager', '@?csrf_token']
form_validator:
class: Drupal\Core\Form\FormValidator
arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form']
......@@ -802,6 +802,10 @@ services:
class: Drupal\Core\EventSubscriber\ExceptionTestSiteSubscriber
tags:
- { name: event_subscriber }
exception.enforced_form_response:
class: Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
tags:
- { name: event_subscriber }
route_processor_manager:
class: Drupal\Core\RouteProcessor\RouteProcessorManager
tags:
......
......@@ -11,6 +11,7 @@
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\ContentNegotiation;
use Drupal\Core\Form\EnforcedResponse;
use Drupal\Core\Page\DefaultHtmlPageRenderer;
use Drupal\Core\Page\HtmlFragment;
use Drupal\Core\Page\HtmlFragmentRendererInterface;
......@@ -201,8 +202,21 @@ protected function createHtmlResponse($title, $body, $response_code) {
$fragment = new HtmlFragment($body);
$fragment->setTitle($title);
$page = $this->fragmentRenderer->render($fragment, $response_code);
return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
// Normally the EnforcedFormResponseSubscriber takes care of the
// EnforcedResponseException. But outside of HttpKernel::handleRaw(), it is
// necessary to catch and handle it manually.
try {
$page = $this->fragmentRenderer->render($fragment, $response_code);
return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
}
catch (\Exception $e) {
if ($response = EnforcedResponse::createFromException($e)) {
return $response;
}
else {
throw $e;
}
}
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Form\EnforcedResponse;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Handle the EnforcedResponseException and deliver an EnforcedResponse.
*/
class EnforcedFormResponseSubscriber implements EventSubscriberInterface {
/**
* Replaces the response in case an EnforcedResponseException was thrown.
*/
public function onKernelException(GetResponseForExceptionEvent $event) {
if ($response = EnforcedResponse::createFromException($event->getException())) {
// Setting the response stops the event propagation.
$event->setResponse($response);
}
}
/**
* Unwraps an enforced response.
*/
public function onKernelResponse(FilterResponseEvent $event) {
$response = $event->getResponse();
if ($response instanceof EnforcedResponse && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
$event->setResponse($response->getResponse());
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::EXCEPTION] = array('onKernelException', 128);
$events[KernelEvents::RESPONSE] = array('onKernelResponse', 128);
return $events;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\EnforcedResponse.
*/
namespace Drupal\Core\Form;
use Symfony\Component\HttpFoundation\Response;
/**
* A wrapper containing a response which is to be enforced upon delivery.
*
* The FormBuilder throws an EnforcedResponseException whenever a form
* desires to explicitly set a response object. Exception handlers capable of
* setting the response should extract the response object of such an exception
* using EnforcedResponse::createFromException(). Then wrap it into an
* EnforcedResponse object and replace the original response with the wrapped
* response.
*
* @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber::onKernelException()
* @see Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::createHtmlResponse()
* @see Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::createResponse()
*/
class EnforcedResponse extends Response {
/**
* The wrapped response object.
*
* @var \Symfony\Component\HttpFoundation\Response;
*/
protected $response;
/**
* Constructs a new enforced response from the given exception.
*
* Note that it is necessary to traverse the exception chain when searching
* for an enforced response. Otherwise it would be impossible to find an
* exception thrown from within a twig template.
*
* @param \Exception $e
* The exception where the enforced response is to be extracted from.
*
* @return \Drupal\Core\Form\EnforcedResponse|NULL
* The enforced response or NULL if the exception chain does not contain a
* \Drupal\Core\Form\EnforcedResponseException exception.
*/
public static function createFromException(\Exception $e) {
while ($e) {
if ($e instanceof EnforcedResponseException) {
return new static($e->getResponse());
}
$e = $e->getPrevious();
}
}
/**
* Constructs an enforced response.
*
* Use EnforcedResponse::createFromException() instead.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* The response to wrap.
*/
public function __construct(Response $response) {
parent::__construct('', 500);
$this->response = $response;
}
/**
* Returns the wrapped response.
*
* @return \Symfony\Component\HttpFoundation\Response
* The wrapped response.
*/
public function getResponse() {
return $this->response;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\EnforcedResponseException.
*/
namespace Drupal\Core\Form;
use Symfony\Component\HttpFoundation\Response;
/**
* Custom exception to break out of the main request and enforce a response.
*/
class EnforcedResponseException extends \Exception {
/**
* The response to be enforced.
*
* @var \Symfony\Component\HttpFoundation\Response
*/
protected $response;
/**
* Constructs a new enforced response exception.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* The response to be enforced.
* @param string $message
* (optional) The exception message.
* @param int $code
* (optional) A user defined exception code.
* @param \Exception $previous
* (optional) The previous exception for nested exceptions
*/
public function __construct(Response $response, $message = "", $code = 0, \Exception $previous = NULL) {
parent::__construct($message, $code, $previous);
$this->response = $response;
}
/**
* Return the response to be enforced.
*
* @returns \Symfony\Component\HttpFoundation\Response $response
* The response to be enforced.
*/
public function getResponse() {
return $this->response;
}
}
......@@ -14,7 +14,6 @@
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Site\Settings;
......@@ -22,9 +21,6 @@
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Provides form building and processing.
......@@ -61,17 +57,6 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
*/
protected $csrfToken;
/**
* The kernel to handle forms returning response objects.
*
* Explicitly use the DrupalKernel as that is consistent with index.php for
* terminating the request and in case someone rebuilds the container,
* this kernel is synthetic and always points to the new container.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* The class resolver.
*
......@@ -131,10 +116,8 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
* The theme manager.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The kernel.
*/
public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ThemeManagerInterface $theme_manager, CsrfTokenGenerator $csrf_token = NULL, DrupalKernelInterface $kernel = NULL) {
public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ThemeManagerInterface $theme_manager, CsrfTokenGenerator $csrf_token = NULL) {
$this->formValidator = $form_validator;
$this->formSubmitter = $form_submitter;
$this->formCache = $form_cache;
......@@ -143,7 +126,6 @@ public function __construct(FormValidatorInterface $form_validator, FormSubmitte
$this->requestStack = $request_stack;
$this->classResolver = $class_resolver;
$this->csrfToken = $csrf_token;
$this->kernel = $kernel;
$this->themeManager = $theme_manager;
}
......@@ -265,10 +247,17 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
// can use it to know or update information about the state of the form.
$response = $this->processForm($form_id, $form, $form_state);
// If the form returns some kind of response, deliver it.
// If the form returns a response, skip subsequent page construction by
// throwing an exception.
// @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
//
// @todo Exceptions should not be used for code flow control. However, the
// Form API does not integrate with the HTTP Kernel based architecture of
// Drupal 8. In order to resolve this issue properly it is necessary to
// completely separate form submission from rendering.
// @see https://www.drupal.org/node/2367555
if ($response instanceof Response) {
$this->sendResponse($response);
exit;
throw new EnforcedResponseException($response);
}
// If this was a successful submission of a single-step form or the last
......@@ -416,10 +405,16 @@ public function retrieveForm($form_id, FormStateInterface &$form_state) {
$args = array_merge(array($form, &$form_state), $args);
$form = call_user_func_array($callback, $args);
// If the form returns some kind of response, deliver it.
// If the form returns a response, skip subsequent page construction by
// throwing an exception.
// @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
//
// @todo Exceptions should not be used for code flow control. However, the
// Form API currently allows any form builder functions to return a
// response.
// @see https://www.drupal.org/node/2363189
if ($form instanceof Response) {
$this->sendResponse($form);
exit;
throw new EnforcedResponseException($form);
}
$form['#form_id'] = $form_id;
......@@ -1080,24 +1075,6 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
return FALSE;
}
/**
* Triggers kernel.response and sends a form response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* A response object.
*/
protected function sendResponse(Response $response) {
$request = $this->requestStack->getCurrentRequest();
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
// Prepare and send the response.
$event->getResponse()
->prepare($request)
->send();
$this->kernel->terminate($request, $response);
}
/**
* Wraps element_info().
*
......
<?php
/**
* @file
* Definition of Drupal\system\Tests\Form\ResponseTest.
*/
namespace Drupal\system\Tests\Form;
use Drupal\Component\Serialization\Json;
use Drupal\simpletest\WebTestBase;
/**
* Tests the form API Response element.
*
* @group Form
*/
class ResponseTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('form_test');
/**
* Tests that enforced responses propagate through subscribers and middleware.
*/
public function testFormResponse() {
$edit = [
'content' => $this->randomString(),
'status' => 200,
];
$content = Json::decode($this->drupalPostForm('form-test/response', $edit, 'Submit'));
$this->assertResponse(200);
$this->assertIdentical($edit['content'], $content, 'Response content matches');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Response-Event'), 'Response handled by kernel response subscriber');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Stack-Middleware'), 'Response handled by kernel middleware');
$edit = [
'content' => $this->randomString(),
'status' => 418,
];
$content = Json::decode($this->drupalPostForm('form-test/response', $edit, 'Submit'));
$this->assertResponse(418);
$this->assertIdentical($edit['content'], $content, 'Response content matches');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Response-Event'), 'Response handled by kernel response subscriber');
$this->assertIdentical('invoked', $this->drupalGetHeader('X-Form-Test-Stack-Middleware'), 'Response handled by kernel middleware');
}
}
......@@ -294,6 +294,14 @@ form_test.url:
requirements:
_access: 'TRUE'
form_test.response:
path: '/form-test/response'
defaults:
_form: '\Drupal\form_test\Form\FormTestResponseForm'
_title: 'Response'
requirements:
_access: 'TRUE'
form_test.disabled_elements:
path: '/form-test/disabled-elements'
defaults:
......
......@@ -5,3 +5,7 @@ services:
class: Drupal\form_test\EventSubscriber\FormTestEventSubscriber
tags:
- { name: event_subscriber }
form_test.http_middleware:
class: Drupal\form_test\StackMiddleware\FormTestMiddleware
tags:
- { name: http_middleware, priority: 0 }
......@@ -8,6 +8,7 @@
namespace Drupal\form_test\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
......@@ -28,11 +29,23 @@ public function onKernelRequest(GetResponseEvent $event) {
$request->attributes->set('request_attribute', 'request_value');
}
/**
* Adds custom headers to the response.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The kernel request event.
*/
public function onKernelResponse(FilterResponseEvent $event) {
$response = $event->getResponse();
$response->headers->set('X-Form-Test-Response-Event', 'invoked');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequest');
$events[KernelEvents::RESPONSE][] = array('onKernelResponse');
return $events;
}
......
<?php
/**
* @file
* Contains \Drupal\form_test\Form\FormTestResponseForm.
*/
namespace Drupal\form_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Form constructor for testing #type 'url' elements.
*/
class FormTestResponseForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'form_test_response';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['content'] = array(
'#type' => 'textfield',
'#title' => 'Content',
);
$form['status'] = array(
'#type' => 'textfield',
'#title' => 'Status',
'#default_value' => 200,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Submit',
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$form_state->setResponse(new JsonResponse($values['content'], (int) $values['status']));
}
}
<?php
/**
* @file
* Contains \Drupal\form_test\StackMiddleware\FormTestMiddleware
*/
namespace Drupal\form_test\StackMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Provides a test middleware which sets a custom response header.
*/
class FormTestMiddleware implements HttpKernelInterface {
/**
* The decorated kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* Constructs a FormTestMiddleware object.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* The decorated kernel.
*/
public function __construct(HttpKernelInterface $http_kernel) {
$this->httpKernel = $http_kernel;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
$response = $this->httpKernel->handle($request, $type, $catch);
$response->headers->set('X-Form-Test-Stack-Middleware', 'invoked');
return $response;
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Tests\Core\Form {
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
......@@ -115,9 +116,6 @@ public function testHandleFormStateResponse($class, $form_state_key) {
$response = $this->getMockBuilder($class)
->disableOriginalConstructor()
->getMock();
$response->expects($this->any())
->method('prepare')
->will($this->returnValue($response));
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_arg->expects($this->any())
......@@ -131,12 +129,12 @@ public function testHandleFormStateResponse($class, $form_state_key) {
$input['form_id'] = $form_id;
$form_state->setUserInput($input);
$this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);
$this->fail('TestFormBuilder::sendResponse() was not triggered.');
$this->fail('EnforcedResponseException was not thrown.');
}
catch (\Exception $e) {
$this->assertSame('exit', $e->getMessage());
catch (EnforcedResponseException $e) {
$this->assertSame($response, $e->getResponse());
}
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $form_state->getResponse());
$this->assertSame($response, $form_state->getResponse());
}
/**
......@@ -160,16 +158,11 @@ public function testHandleRedirectWithResponse() {
$response = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response')
->disableOriginalConstructor()
->getMock();
$response->expects($this->once())
->method('prepare')
->will($this->returnValue($response));
// Set up a redirect that will not be called.
$redirect = $this->getMockBuilder('Symfony\Component\HttpFoundation\RedirectResponse')
->disableOriginalConstructor()
->getMock();
$redirect->expects($this->never())
->method('prepare');
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_arg->expects($this->any())
......@@ -185,10 +178,10 @@ public function testHandleRedirectWithResponse() {
$input['form_id'] = $form_id;
$form_state->setUserInput($input);
$this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);
$this->fail('TestFormBuilder::sendResponse() was not triggered.');
$this->fail('EnforcedResponseException was not thrown.');
}
catch (\Exception $e) {
$this->assertSame('exit', $e->getMessage());
catch (EnforcedResponseException $e) {
$this->assertSame($response, $e->getResponse());
}
$this->assertSame($response, $form_state->getResponse());
}
......@@ -370,9 +363,6 @@ public function testSendResponse() {
$expected_form = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response')
->disableOriginalConstructor()
->getMock();
$expected_form->expects($this->once())
->method('prepare')
->will($this->returnValue($expected_form));
$form_arg = $this->getMockForm($form_id, $expected_form);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment