diff --git a/core/core.services.yml b/core/core.services.yml index 6f8cce6..bb7a688 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -182,6 +182,7 @@ services: factory_class: Symfony\Component\HttpFoundation\Request factory_method: createFromGlobals #synthetic: true + synchronized: true event_dispatcher: class: Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher arguments: ['@service_container'] @@ -327,17 +328,17 @@ services: arguments: ['@content_negotiation'] tags: - { name: route_enhancer, priority: 30 } - route_enhancer.ajax: - class: Drupal\Core\Routing\Enhancer\AjaxEnhancer + route_enhancer.entity: + class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer arguments: ['@content_negotiation'] tags: - { name: route_enhancer, priority: 20 } - - { name: legacy_route_enhancer, priority: 20 } - route_enhancer.entity: - class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer + route_enhancer.ajax: + class: Drupal\Core\Routing\Enhancer\AjaxEnhancer arguments: ['@content_negotiation'] tags: - { name: route_enhancer, priority: 15 } + - { name: legacy_route_enhancer, priority: 20 } route_enhancer.form: class: Drupal\Core\Routing\Enhancer\FormEnhancer arguments: ['@content_negotiation'] @@ -345,10 +346,13 @@ services: - { name: route_enhancer, priority: 10 } controller.page: class: Drupal\Core\Controller\HtmlPageController - arguments: ['@http_kernel'] + arguments: ['@controller_resolver'] controller.dialog: class: Drupal\Core\Controller\DialogController - arguments: ['@http_kernel'] + arguments: ['@controller_resolver'] + controller.ajax: + class: Drupal\Core\Controller\AjaxController + arguments: ['@controller_resolver'] router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener tags: @@ -409,13 +413,11 @@ services: tags: - { name: event_subscriber } arguments: ['@language_manager'] - scope: request redirect_response_subscriber: class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber arguments: ['@url_generator'] tags: - { name: event_subscriber } - scope: request request_close_subscriber: class: Drupal\Core\EventSubscriber\RequestCloseSubscriber tags: diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 8e024a8..1bced44 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -23,9 +23,9 @@ * forms, it can be used with the #ajax property. * The #ajax property can be used to bind events to the Ajax framework. By * default, #ajax uses 'system/ajax' as its path for submission and thus calls - * ajax_form_callback() and a defined #ajax['callback'] function. - * However, you may optionally specify a different path to request or a - * different callback function to invoke, which can return updated HTML or can + * \Drupal\system\FormAjaxController::content() and a defined #ajax['callback'] + * function. However, you may optionally specify a different path to request or + * a different callback function to invoke, which can return updated HTML or can * also return a richer set of * @link ajax_commands Ajax framework commands @endlink. * @@ -38,17 +38,18 @@ * that element. * - The browser submits an HTTP POST request to the 'system/ajax' Drupal * path. - * - The menu page callback for 'system/ajax', ajax_form_callback(), calls - * drupal_process_form() to process the form submission and rebuild the - * form if necessary. The form is processed in much the same way as if it - * were submitted without Ajax, with the same #process functions and - * validation and submission handlers called in either case, making it easy - * to create Ajax-enabled forms that degrade gracefully when JavaScript is - * disabled. - * - After form processing is complete, ajax_form_callback() calls the - * function named by #ajax['callback'], which returns the form element that - * has been updated and needs to be returned to the browser, or - * alternatively, an array of custom Ajax commands. + * - The controller for the route '/system/ajax', + * \Drupal\system\FormAjaxController::content(), calls drupal_process_form() + * to process the form submission and rebuild the form if necessary. The + * form is processed in much the same way as if it were submitted without + * Ajax, with the same #process functions and validation and submission + * handlers called in either case, making it easy to create Ajax-enabled + * forms that degrade gracefully when JavaScript is disabled. + * - After form processing is complete, + * \Drupal\system\FormAjaxController::content() calls the function named by + * #ajax['callback'], which returns the form element that has been updated + * and needs to be returned to the browser, or alternatively, an array of + * custom Ajax commands. * - The array is serialized using ajax_render() and sent to the browser. * - The browser unserializes the returned JSON string into an array of * command objects and executes each command, resulting in the old page @@ -125,10 +126,10 @@ * - #ajax['path']: The menu path to use for the request. This is often omitted * and the default is used. This path should map * to a menu page callback that returns data using ajax_render(). Defaults to - * 'system/ajax', which invokes ajax_form_callback(), eventually calling - * the function named in #ajax['callback']. If you use a custom - * path, you must set up the menu entry and handle the entire callback in your - * own code. + * 'system/ajax', which invokes \Drupal\system\FormAjaxController::content(), + * eventually calling the function named in #ajax['callback']. If you use a + * custom path, you must set up the menu entry and handle the entire callback + * in your own code. * - #ajax['wrapper']: The CSS ID of the area to be replaced by the content * returned by the #ajax['callback'] function. The content returned from * the callback will replace the entire element named by #ajax['wrapper']. @@ -302,88 +303,6 @@ function ajax_render($commands = array()) { } /** - * Gets a form submitted via #ajax during an Ajax callback. - * - * This will load a form from the form cache used during Ajax operations. It - * pulls the form info from $_POST. - * - * @return - * An array containing the $form and $form_state. Use the list() function - * to break these apart: - * @code - * list($form, $form_state, $form_id, $form_build_id) = ajax_get_form(); - * @endcode - */ -function ajax_get_form() { - $form_state = form_state_defaults(); - - $form_build_id = $_POST['form_build_id']; - - // Get the form from the cache. - $form = form_get_cache($form_build_id, $form_state); - if (!$form) { - // If $form cannot be loaded from the cache, the form_build_id in $_POST - // must be invalid, which means that someone performed a POST request onto - // system/ajax without actually viewing the concerned form in the browser. - // This is likely a hacking attempt as it never happens under normal - // circumstances, so we just do nothing. - watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING); - throw new BadRequestHttpException(); - } - - // Since some of the submit handlers are run, redirects need to be disabled. - $form_state['no_redirect'] = TRUE; - - // When a form is rebuilt after Ajax processing, its #build_id and #action - // should not change. - // @see drupal_rebuild_form() - $form_state['rebuild_info']['copy']['#build_id'] = TRUE; - $form_state['rebuild_info']['copy']['#action'] = TRUE; - - // The form needs to be processed; prepare for that by setting a few internal - // variables. - $form_state['input'] = $_POST; - $form_id = $form['#form_id']; - - return array($form, $form_state, $form_id, $form_build_id); -} - -/** - * Page callback: Handles Ajax requests for the #ajax Form API property. - * - * This rebuilds the form from cache and invokes the defined #ajax['callback'] - * to return an Ajax command structure for JavaScript. In case no 'callback' has - * been defined, nothing will happen. - * - * The Form API #ajax property can be set both for buttons and other input - * elements. - * - * This function is also the canonical example of how to implement - * #ajax['path']. If processing is required that cannot be accomplished with - * a callback, re-implement this function and set #ajax['path'] to the - * enhanced function. - * - * @see system_menu() - */ -function ajax_form_callback() { - list($form, $form_state) = ajax_get_form(); - drupal_process_form($form['#form_id'], $form, $form_state); - - // We need to return the part of the form (or some other content) that needs - // to be re-rendered so the browser can update the page with changed content. - // Since this is the generic menu callback used by many Ajax elements, it is - // up to the #ajax['callback'] function of the element (may or may not be a - // button) that triggered the Ajax request to determine what needs to be - // rendered. - if (!empty($form_state['triggering_element'])) { - $callback = $form_state['triggering_element']['#ajax']['callback']; - } - if (!empty($callback) && is_callable($callback)) { - return call_user_func_array($callback, array(&$form, &$form_state)); - } -} - -/** * Theme callback: Returns the correct theme for an Ajax request. * * Many different pages can invoke an Ajax request to system/ajax or another diff --git a/core/includes/form.inc b/core/includes/form.inc index 5abf669..ba22c78 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -471,8 +471,9 @@ function form_state_defaults() { * workflow, to be returned for rendering. * * Ajax form submissions are almost always multi-step workflows, so that is one - * common use-case during which form rebuilding occurs. See ajax_form_callback() - * for more information about creating Ajax-enabled forms. + * common use-case during which form rebuilding occurs. See + * Drupal\system\FormAjaxController::content() for more information about + * creating Ajax-enabled forms. * * @param $form_id * The unique string identifying the desired form. If a function @@ -496,7 +497,7 @@ function form_state_defaults() { * The newly built form. * * @see drupal_process_form() - * @see ajax_form_callback() + * @see \Drupal\system\FormAjaxController::content() */ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { $form = drupal_retrieve_form($form_id, $form_state); @@ -1292,9 +1293,10 @@ function drupal_validate_form($form_id, &$form, &$form_state) { * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no * redirection is done. * - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is - * set, for instance, by ajax_get_form() to prevent redirection in Ajax - * callbacks. $form_state['no_redirect'] should never be set or altered by - * form builder functions or form validation/submit handlers. + * set, for instance, by \Drupal\system\FormAjaxController::getForm() to + * prevent redirection in Ajax callbacks. $form_state['no_redirect'] should + * never be set or altered by form builder functions or form validation/submit + * handlers. * - If $form_state['redirect'] is set to FALSE, redirection is disabled. * - If none of the above conditions has prevented redirection, then the * redirect is accomplished by returning a RedirectResponse, passing in the diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php index b4dc5a1..5dd287d 100644 --- a/core/lib/Drupal/Core/Controller/AjaxController.php +++ b/core/lib/Drupal/Core/Controller/AjaxController.php @@ -10,13 +10,30 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Ajax\PrependCommand; -use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Default controller for ajax requests. */ -class AjaxController extends ContainerAware { +class AjaxController { + + /** + * The controller resolver. + * + * @var \Drupal\Core\Controller\ControllerResolver + */ + protected $controllerResolver; + + /** + * Constructs a HtmlPageController instance. + * + *@param \Drupal\Core\Controller\ControllerResolver $controller_resolver + * The controller resolver. + */ + public function __construct(ControllerResolver $controller_resolver) { + $this->controllerResolver = $controller_resolver; + } /** * Controller method for AJAX content. @@ -31,49 +48,34 @@ class AjaxController extends ContainerAware { */ public function content(Request $request, $_content) { - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - $controller = $_content; - - // We need to clean up the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('system_path'); - $attributes->remove('_content'); - $attributes->remove('_legacy'); - - // Remove the accept header so the subrequest does not end up back in this - // controller. - $request->headers->remove('accept'); - // Remove the header in order to let the subrequest not think that it's an - // ajax request, see \Drupal\Core\ContentNegotiation. - $request->headers->remove('x-requested-with'); - - $response = $this->container->get('http_kernel')->forward($controller, $attributes->all(), $request->query->all()); - // For successful (HTTP status 200) responses. - if ($response->isOk()) { - // If there is already an AjaxResponse, then return it without - // manipulation. - if (!($response instanceof AjaxResponse)) { + $callable = $this->controllerResolver->createController($_content); + $arguments = $this->controllerResolver->getArguments($request, $callable); + $response = call_user_func_array($callable, $arguments); + if ($response instanceof Response) { + if ($response instanceof AjaxResponse || !$response->isOk()) { + return $response; + } + else { // Pull the content out of the response. $content = $response->getContent(); - // A page callback could return a render array or a string. - $html = is_string($content) ? $content : drupal_render($content); - $response = new AjaxResponse(); - // The selector for the insert command is NULL as the new content will - // replace the element making the ajax call. The default 'replaceWith' - // behavior can be changed with #ajax['method']. - $response->addCommand(new InsertCommand(NULL, $html)); - $status_messages = array('#theme' => 'status_messages'); - $output = drupal_render($status_messages); - if (!empty($output)) { - $response->addCommand(new PrependCommand(NULL, $output)); - } } } + else { + $content = $response; + } + + // A page callback could return a render array or a string. + $html = is_string($content) ? $content : drupal_render($content); + $response = new AjaxResponse(); + // The selector for the insert command is NULL as the new content will + // replace the element making the ajax call. The default 'replaceWith' + // behavior can be changed with #ajax['method']. + $response->addCommand(new InsertCommand(NULL, $html)); + $status_messages = array('#theme' => 'status_messages'); + $output = drupal_render($status_messages); + if (!empty($output)) { + $response->addCommand(new PrependCommand(NULL, $output)); + } return $response; } } diff --git a/core/lib/Drupal/Core/Controller/ControllerResolver.php b/core/lib/Drupal/Core/Controller/ControllerResolver.php index 2fd4ee2..ad42c6c 100644 --- a/core/lib/Drupal/Core/Controller/ControllerResolver.php +++ b/core/lib/Drupal/Core/Controller/ControllerResolver.php @@ -66,7 +66,7 @@ public function __construct(ContainerInterface $container, LoggerInterface $logg * @throws \InvalidArgumentException * If the controller class does not exist */ - protected function createController($controller) { + public function createController($controller) { // Controller in the service:method notation. $count = substr_count($controller, ':'); if ($count == 1) { diff --git a/core/lib/Drupal/Core/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php index 365c196..f8cc186 100644 --- a/core/lib/Drupal/Core/Controller/DialogController.php +++ b/core/lib/Drupal/Core/Controller/DialogController.php @@ -19,51 +19,20 @@ class DialogController { /** - * The HttpKernel object to use for subrequests. + * The controller resolver. * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface + * @var \Drupal\Core\Controller\ControllerResolver */ - protected $httpKernel; + protected $controllerResolver; /** * Constructs a new DialogController. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel - * The kernel. + *@param \Drupal\Core\Controller\ControllerResolver $controller_resolver + * The controller resolver. */ - public function __construct(HttpKernelInterface $kernel) { - $this->httpKernel = $kernel; - } - - /** - * Forwards request to a subrequest. - * - * @param \Symfony\Component\HttpFoundation\RequestRequest $request - * The request object. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - protected function forward(Request $request) { - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - // We need to clean up the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('system_path'); - $attributes->set('dialog', TRUE); - - // Remove the accept header so the subrequest does not end up back in this - // controller. - $request->headers->remove('accept'); - // Remove the X-Requested-With header so the subrequest is not mistaken for - // an ajax request. - $request->headers->remove('x-requested-with'); - - return $this->httpKernel->forward(NULL, $attributes->all(), $request->query->all()); + public function __construct(ControllerResolver $controller_resolver) { + $this->controllerResolver = $controller_resolver; } /** @@ -75,14 +44,14 @@ protected function forward(Request $request) { * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a modal dialog. */ - public function modal(Request $request) { - return $this->dialog($request, TRUE); + public function modal(Request $request, $_content) { + return $this->dialog($request, $_content, TRUE); } /** * Displays content in a dialog. * - * @param \Symfony\Component\HttpFoundation\RequestRequest $request + * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param bool $modal * (optional) TRUE to render a modal dialog. Defaults to FALSE. @@ -90,10 +59,12 @@ public function modal(Request $request) { * @return \Drupal\Core\Ajax\AjaxResponse * AjaxResponse to return the content wrapper in a dialog. */ - public function dialog(Request $request, $modal = FALSE) { - $subrequest = $this->forward($request); - if ($subrequest->isOk()) { - $content = $subrequest->getContent(); + public function dialog(Request $request, $_content, $modal = FALSE) { + $callable = $this->controllerResolver->createController($_content); + $arguments = $this->controllerResolver->getArguments($request, $callable); + $controller_response = call_user_func_array($callable, $arguments); + if ($controller_response->isOk()) { + $content = $controller_response->getContent(); // @todo Remove use of drupal_get_title() when // http://drupal.org/node/1871596 is in. $title = drupal_get_title(); @@ -129,6 +100,6 @@ public function dialog(Request $request, $modal = FALSE) { return $response; } // An error occurred in the subrequest, return that. - return $subrequest; + return $controller_response; } } diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php index fd89081..6a9fbc0 100644 --- a/core/lib/Drupal/Core/Controller/HtmlPageController.php +++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Controller; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -17,19 +18,20 @@ class HtmlPageController { /** - * The HttpKernel object to use for subrequests. + * The controller resolver. * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface + * @var \Drupal\Core\Controller\ControllerResolver */ - protected $httpKernel; + protected $controllerResolver; /** - * Constructs a new HtmlPageController. + * Constructs a HtmlPageController instance. * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel + *@param \Drupal\Core\Controller\ControllerResolver $controller_resolver + * The controller resolver. */ - public function __construct(HttpKernelInterface $kernel) { - $this->httpKernel = $kernel; + public function __construct(ControllerResolver $controller_resolver) { + $this->controllerResolver = $controller_resolver; } /** @@ -44,28 +46,18 @@ public function __construct(HttpKernelInterface $kernel) { * A response object. */ public function content(Request $request, $_content) { - - // @todo When we have a Generator, we can replace the forward() call with - // a render() call, which would handle ESI and hInclude as well. That will - // require an _internal route. For examples, see: - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml - // https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php - $attributes = clone $request->attributes; - $controller = $_content; - - // We need to clean off the derived information and such so that the - // subrequest can be processed properly without leaking data through. - $attributes->remove('system_path'); - $attributes->remove('_content'); - - $response = $this->httpKernel->forward($controller, $attributes->all(), $request->query->all()); - - // For successful (HTTP status 200) responses, decorate with blocks. - if ($response->isOk()) { - $page_content = $response->getContent(); - $response = new Response(drupal_render_page($page_content)); + $callable = $this->controllerResolver->createController($_content); + $arguments = $this->controllerResolver->getArguments($request, $callable); + $page_content = call_user_func_array($callable, $arguments); + if ($page_content instanceof Response) { + return $page_content; } - + if (!is_array($page_content)) { + $page_content = array( + '#markup' => $page_content, + ); + } + $response = new Response(drupal_render_page($page_content)); return $response; } } diff --git a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php index f2f6648..eadb1ea 100644 --- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php +++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php @@ -37,7 +37,7 @@ public function __construct(ContentNegotiation $negotiation) { * {@inheritdoc} */ public function enhance(array $defaults, Request $request) { - if (empty($defaults['_controller']) && $this->negotiation->getContentType($request) === 'html') { + if (empty($defaults['_controller']) && in_array($this->negotiation->getContentType($request), array('html', 'drupal_ajax'))) { if (!empty($defaults['_entity_form'])) { $defaults['_controller'] = '\Drupal\Core\Entity\HtmlEntityFormController::content'; } diff --git a/core/lib/Drupal/Core/HttpKernel.php b/core/lib/Drupal/Core/HttpKernel.php index 07c79c7..ec426c7 100644 --- a/core/lib/Drupal/Core/HttpKernel.php +++ b/core/lib/Drupal/Core/HttpKernel.php @@ -2,12 +2,7 @@ /** * @file - * Definition of Drupal\Core\HttpKernel. - * - * @todo This file is copied verbatim, with the exception of the namespace - * change and this commment block, from Symfony full stack's FrameworkBundle. - * Once the FrameworkBundle is available as a Composer package we should switch - * to pulling it via Composer. + * Contains \Drupal\Core\HttpKernel. */ namespace Drupal\Core; @@ -23,249 +18,42 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** - * This HttpKernel is used to manage scope changes of the DI container. - * - * @author Fabien Potencier - * @author Johannes M. Schmitt + * HttpKernel extension to allow easy in-process subrequests. */ -class HttpKernel extends BaseHttpKernel -{ - protected $container; +class HttpKernel extends BaseHttpKernel { - private $esiSupport; - - public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver) - { - parent::__construct($dispatcher, $controllerResolver); - - $this->container = $container; - } - - public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) - { - $request->headers->set('X-Php-Ob-Level', ob_get_level()); - - $this->container->enterScope('request'); - $this->container->set('request', $request, 'request'); - - try { - $response = parent::handle($request, $type, $catch); - } catch (\Exception $e) { - $this->container->leaveScope('request'); - - throw $e; - } - - $this->container->leaveScope('request'); - - return $response; - } - - /** - * Forwards the request to another controller. - * - * @param string|NULL $controller - * The controller name (a string like BlogBundle:Post:index). - * @param array $attributes - * An array of request attributes. - * @param array $query - * An array of request query parameters. - * - * @return Response - * A Response instance - */ - public function forward($controller, array $attributes = array(), array $query = array()) - { - $subrequest = $this->setupSubrequest($controller, $attributes, $query); - - return $this->handle($subrequest, HttpKernelInterface::SUB_REQUEST); - } - - /** - * Renders a Controller and returns the Response content. - * - * Note that this method generates an esi:include tag only when both the standalone - * option is set to true and the request has ESI capability (@see Symfony\Component\HttpKernel\HttpCache\ESI). - * - * Available options: - * - * * attributes: An array of request attributes (only when the first argument is a controller) - * * query: An array of request query parameters (only when the first argument is a controller) - * * ignore_errors: true to return an empty string in case of an error - * * alt: an alternative controller to execute in case of an error (can be a controller, a URI, or an array with the controller, the attributes, and the query arguments) - * * standalone: whether to generate an esi:include tag or not when ESI is supported - * * comment: a comment to add when returning an esi:include tag - * - * @param string $controller A controller name to execute (a string like BlogBundle:Post:index), or a relative URI - * @param array $options An array of options - * - * @return string The Response content - */ - public function render($controller, array $options = array()) - { - $options = array_merge(array( - 'attributes' => array(), - 'query' => array(), - 'ignore_errors' => !$this->container->getParameter('kernel.debug'), - 'alt' => array(), - 'standalone' => false, - 'comment' => '', - ), $options); - - if (!is_array($options['alt'])) { - $options['alt'] = array($options['alt']); - } - - if (null === $this->esiSupport) { - $this->esiSupport = $this->container->has('esi') && $this->container->get('esi')->hasSurrogateEsiCapability($this->container->get('request')); - } - - if ($this->esiSupport && (true === $options['standalone'] || 'esi' === $options['standalone'])) { - $uri = $this->generateInternalUri($controller, $options['attributes'], $options['query']); - - $alt = ''; - if ($options['alt']) { - $alt = $this->generateInternalUri($options['alt'][0], isset($options['alt'][1]) ? $options['alt'][1] : array(), isset($options['alt'][2]) ? $options['alt'][2] : array()); - } - - return $this->container->get('esi')->renderIncludeTag($uri, $alt, $options['ignore_errors'], $options['comment']); - } - - if ('js' === $options['standalone']) { - $uri = $this->generateInternalUri($controller, $options['attributes'], $options['query'], false); - $defaultContent = null; - - if ($template = $this->container->getParameter('templating.hinclude.default_template')) { - $defaultContent = $this->container->get('templating')->render($template); - } - - return $this->renderHIncludeTag($uri, $defaultContent); - } - - $request = $this->container->get('request'); - - // controller or URI? - if (0 === strpos($controller, '/')) { - $subRequest = Request::create($request->getUriForPath($controller), 'get', array(), $request->cookies->all(), array(), $request->server->all()); - if ($session = $request->getSession()) { - $subRequest->setSession($session); - } - } else { - $options['attributes']['_controller'] = $controller; - - if (!isset($options['attributes']['_format'])) { - $options['attributes']['_format'] = $request->getRequestFormat(); - } - - $options['attributes'][RouteObjectInterface::ROUTE_OBJECT] = '_internal'; - $subRequest = $request->duplicate($options['query'], null, $options['attributes']); - $subRequest->setMethod('GET'); - } - - $level = ob_get_level(); - try { - $response = $this->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); - - if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $request->getUri(), $response->getStatusCode())); - } - - if (!$response instanceof StreamedResponse) { - return $response->getContent(); - } - - $response->sendContent(); - } catch (\Exception $e) { - if ($options['alt']) { - $alt = $options['alt']; - unset($options['alt']); - $options['attributes'] = isset($alt[1]) ? $alt[1] : array(); - $options['query'] = isset($alt[2]) ? $alt[2] : array(); - - return $this->render($alt[0], $options); - } - - if (!$options['ignore_errors']) { - throw $e; - } - - // let's clean up the output buffers that were created by the sub-request - while (ob_get_level() > $level) { - ob_get_clean(); - } - } - } - - /** - * Generates an internal URI for a given controller. - * - * This method uses the "_internal" route, which should be available. - * - * @param string $controller A controller name to execute (a string like BlogBundle:Post:index), or a relative URI - * @param array $attributes An array of request attributes - * @param array $query An array of request query parameters - * @param boolean $secure - * - * @return string An internal URI - */ - public function generateInternalUri($controller, array $attributes = array(), array $query = array(), $secure = true) - { - if (0 === strpos($controller, '/')) { - return $controller; - } + /** + * Constructs a \Drupal\Core\HttpKernel object. + * + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * The event dispatcher. + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The service container. + * @param \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface $resolver + * The controller resolver. + */ + public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $resolver) { + parent::__construct($dispatcher, $resolver); + $this->container = $container; + } - $path = http_build_query($attributes, '', '&'); - $uri = $this->container->get('router')->generate($secure ? '_internal' : '_internal_public', array( - 'controller' => $controller, - 'path' => $path ?: 'none', - '_format' => $this->container->get('request')->getRequestFormat(), - )); + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { + $request->headers->set('X-Php-Ob-Level', ob_get_level()); - if ($queryString = http_build_query($query, '', '&')) { - $uri .= '?'.$queryString; - } + $this->container->enterScope('request'); + $this->container->set('request', $request, 'request'); - return $uri; + try { + $response = parent::handle($request, $type, $catch); } - - /** - * Renders an HInclude tag. - * - * @param string $uri A URI - * @param string $defaultContent Default content - */ - public function renderHIncludeTag($uri, $defaultContent = null) - { - return sprintf('%s', $uri, $defaultContent); + catch (\Exception $e) { + $this->container->leaveScope('request'); + throw $e; } - public function hasEsiSupport() - { - return $this->esiSupport; - } + $this->container->leaveScope('request'); - /** - * Creates a request object for a subrequest. - * - * @param string $controller - * The controller name (a string like BlogBundle:Post:index) - * @param array $attributes - * An array of request attributes. - * @param array $query - * An array of request query parameters. - * - * @return \Symfony\Component\HttpFoundation\Request - * Returns the new request. - */ - public function setupSubrequest($controller, array $attributes, array $query) { - // Don't override the controller if it's NULL. - if (isset($controller)) { - $attributes['_controller'] = $controller; - } - else { - unset($attributes['_controller']); - } - return $this->container->get('request')->duplicate($query, NULL, $attributes); + return $response; } } diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php index 47c5179..7c4a1df 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php @@ -39,7 +39,7 @@ public function __construct(ContentNegotiation $negotiation) { public function enhance(array $defaults, Request $request) { if (empty($defaults['_content']) && $this->negotiation->getContentType($request) == 'drupal_ajax') { $defaults['_content'] = isset($defaults['_controller']) ? $defaults['_controller'] : NULL; - $defaults['_controller'] = '\Drupal\Core\Controller\AjaxController::content'; + $defaults['_controller'] = 'controller.ajax:content'; } return $defaults; } diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php index e088d44..de2d41f 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/ContentControllerEnhancer.php @@ -58,11 +58,6 @@ public function enhance(array $defaults, Request $request) { $defaults['_controller'] = $this->types[$type]; } } - // When the dialog attribute is TRUE this is a DialogController sub-request. - // Route the sub-request to the _content callable. - if ($request->attributes->get('dialog') && !empty($defaults['_content'])) { - $defaults['_controller'] = $defaults['_content']; - } return $defaults; } } diff --git a/core/modules/field/field.form.inc b/core/modules/field/field.form.inc index ac0cb70..84571eb 100644 --- a/core/modules/field/field.form.inc +++ b/core/modules/field/field.form.inc @@ -129,8 +129,8 @@ function field_form_element_after_build($element, &$form_state) { * This handler is run regardless of whether JS is enabled or not. It makes * changes to the form state. If the button was clicked with JS disabled, then * the page is reloaded with the complete rebuilt form. If the button was - * clicked with JS enabled, then ajax_form_callback() calls field_add_more_js() - * to return just the changed part of the form. + * clicked with JS enabled, then Drupal\system\FormAjaxController::content() + * calls field_add_more_js() to return just the changed part of the form. */ function field_add_more_submit($form, &$form_state) { $button = $form_state['triggering_element']; diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index 7e693c7..d3dc0a0 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -428,12 +428,19 @@ function file_field_widget_process($element, &$form_state, $form) { // file, the entire group of file fields is updated together. if ($element['#cardinality'] != 1) { $parents = array_slice($element['#array_parents'], 0, -1); - $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value']; + $new_path = 'file/ajax'; + $new_options = array( + 'query' => array( + 'element_parents' => implode('/', $parents), + 'form_build_id' => $form['form_build_id']['#value'], + ), + ); $field_element = NestedArray::getValue($form, $parents); $new_wrapper = $field_element['#id'] . '-ajax-wrapper'; foreach (element_children($element) as $key) { if (isset($element[$key]['#ajax'])) { $element[$key]['#ajax']['path'] = $new_path; + $element[$key]['#ajax']['options'] = $new_options; $element[$key]['#ajax']['wrapper'] = $new_wrapper; } } diff --git a/core/modules/file/file.module b/core/modules/file/file.module index c662493..9858bd0 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -46,14 +46,12 @@ function file_menu() { $items = array(); $items['file/ajax'] = array( - 'page callback' => 'file_ajax_upload', - 'access arguments' => array('access content'), + 'route_name' => 'file.ajax.upload', 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); $items['file/progress'] = array( - 'page callback' => 'file_ajax_progress', - 'access arguments' => array('access content'), + 'route name' => 'file.ajax.progress', 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); @@ -960,104 +958,6 @@ function file_save_upload($form_field_name, $validators = array(), $destination } /** - * Ajax callback: Processes file uploads and deletions. - * - * This rebuilds the form element for a particular field item. As long as the - * form processing is properly encapsulated in the widget element the form - * should rebuild correctly using FAPI without the need for additional callbacks - * or processing. - * - * @see file_menu() - */ -function file_ajax_upload() { - $form_parents = func_get_args(); - $form_build_id = (string) array_pop($form_parents); - - $request = \Drupal::request(); - if (!$request->request->has('form_build_id') || $form_build_id != $request->request->get('form_build_id')) { - // Invalid request. - drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); - $response = new AjaxResponse(); - $status_messages = array('#theme' => 'status_messages'); - return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages))); - } - - list($form, $form_state) = ajax_get_form(); - - if (!$form) { - // Invalid form_build_id. - drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error'); - $response = new AjaxResponse(); - $status_messages = array('#theme' => 'status_messages'); - return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages))); - } - - // Get the current element and count the number of files. - $current_element = $form; - foreach ($form_parents as $parent) { - $current_element = $current_element[$parent]; - } - $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0; - - // Process user input. $form and $form_state are modified in the process. - drupal_process_form($form['#form_id'], $form, $form_state); - - // Retrieve the element to be rendered. - foreach ($form_parents as $parent) { - $form = $form[$parent]; - } - - // Add the special Ajax class if a new file was added. - if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { - $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; - } - // Otherwise just add the new content class on a placeholder. - else { - $form['#suffix'] .= ''; - } - - $status_messages = array('#theme' => 'status_messages'); - $form['#prefix'] .= drupal_render($status_messages); - $output = drupal_render($form); - $js = drupal_add_js(); - $settings = drupal_merge_js_settings($js['settings']['data']); - - $response = new AjaxResponse(); - return $response->addCommand(new ReplaceCommand(NULL, $output, $settings)); -} - -/** - * Ajax callback: Retrieves upload progress. - * - * @param $key - * The unique key for this upload process. - */ -function file_ajax_progress($key) { - $progress = array( - 'message' => t('Starting upload...'), - 'percentage' => -1, - ); - - $implementation = file_progress_implementation(); - if ($implementation == 'uploadprogress') { - $status = uploadprogress_get_info($key); - if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) { - $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total']))); - $progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']); - } - } - elseif ($implementation == 'apc') { - $status = apc_fetch('upload_' . $key); - if (isset($status['current']) && !empty($status['total'])) { - $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total']))); - $progress['percentage'] = round(100 * $status['current'] / $status['total']); - } - } - - return new JsonResponse($progress); -} - -/** * Determines the preferred upload progress implementation. * * @return @@ -1239,7 +1139,13 @@ function file_managed_file_process($element, &$form_state, $form) { $element['#tree'] = TRUE; $ajax_settings = array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], + 'path' => 'file/ajax', + 'options' => array( + 'query' => array( + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $form['form_build_id']['#value'], + ), + ), 'wrapper' => $element['#id'] . '-ajax-wrapper', 'effect' => 'fade', 'progress' => array( diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml new file mode 100644 index 0000000..c2a9efd --- /dev/null +++ b/core/modules/file/file.routing.yml @@ -0,0 +1,12 @@ +file.ajax.upload: + pattern: '/file/ajax' + defaults: + _controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload' + requirements: + _permission: 'access content' +file.ajax.progress: + pattern: '/file/progress' + defaults: + _controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress' + requirements: + _permission: 'access content' diff --git a/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php b/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php new file mode 100644 index 0000000..0df85ad --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Controller/FileWidgetAjaxController.php @@ -0,0 +1,119 @@ +query->get('element_parents')); + $form_build_id = $request->query->get('form_build_id'); + $request_form_build_id = $request->request->get('form_build_id'); + + if (empty($request_form_build_id) || $form_build_id !== $request_form_build_id) { + // Invalid request. + drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); + $response = new AjaxResponse(); + $status_messages = array('#theme' => 'status_messages'); + return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages))); + } + + try { + list($form, $form_state) = $this->getForm($request); + } + catch (HttpExceptionInterface $e) { + // Invalid form_build_id. + drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error'); + $response = new AjaxResponse(); + $status_messages = array('#theme' => 'status_messages'); + return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages))); + } + + // Get the current element and count the number of files. + $current_element = NestedArray::getValue($form, $form_parents); + $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0; + + // Process user input. $form and $form_state are modified in the process. + drupal_process_form($form['#form_id'], $form, $form_state); + + // Retrieve the element to be rendered. + $form = NestedArray::getValue($form, $form_parents); + + // Add the special Ajax class if a new file was added. + if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { + $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; + } + // Otherwise just add the new content class on a placeholder. + else { + $form['#suffix'] .= ''; + } + + $status_messages = array('#theme' => 'status_messages'); + $form['#prefix'] .= drupal_render($status_messages); + $output = drupal_render($form); + $js = drupal_add_js(); + $settings = drupal_merge_js_settings($js['settings']['data']); + + $response = new AjaxResponse(); + return $response->addCommand(new ReplaceCommand(NULL, $output, $settings)); + } + + /** + * Returns the progress status for a file upload process. + * + * @param string $key + * The unique key for this upload process. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * A JsonResponse object. + */ + public function progress($key) { + $progress = array( + 'message' => t('Starting upload...'), + 'percentage' => -1, + ); + + $implementation = file_progress_implementation(); + if ($implementation == 'uploadprogress') { + $status = uploadprogress_get_info($key); + if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) { + $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total']))); + $progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']); + } + } + elseif ($implementation == 'apc') { + $status = apc_fetch('upload_' . $key); + if (isset($status['current']) && !empty($status['total'])) { + $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total']))); + $progress['percentage'] = round(100 * $status['current'] / $status['total']); + } + } + + return new JsonResponse($progress); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php b/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php new file mode 100644 index 0000000..7919844 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Controller/FormAjaxController.php @@ -0,0 +1,100 @@ +getForm($request); + drupal_process_form($form['#form_id'], $form, $form_state); + + // We need to return the part of the form (or some other content) that needs + // to be re-rendered so the browser can update the page with changed content. + // Since this is the generic menu callback used by many Ajax elements, it is + // up to the #ajax['callback'] function of the element (may or may not be a + // button) that triggered the Ajax request to determine what needs to be + // rendered. + if (!empty($form_state['triggering_element'])) { + $callback = $form_state['triggering_element']['#ajax']['callback']; + } + if (empty($callback) || !is_callable($callback)) { + throw new HttpException(500, t('Internal Server Error')); + } + return call_user_func_array($callback, array(&$form, &$form_state)); + } + + /** + * Gets a form submitted via #ajax during an Ajax callback. + * + * This will load a form from the form cache used during Ajax operations. It + * pulls the form info from the request body. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return array + * An array containing the $form and $form_state. Use the list() function + * to break these apart: + * @code + * list($form, $form_state, $form_id, $form_build_id) = $this->getForm(); + * @endcode + * + * @throws Symfony\Component\HttpKernel\Exception\HttpExceptionInterface + */ + protected function getForm(Request $request) { + $form_state = form_state_defaults(); + $form_build_id = $request->request->get('form_build_id'); + + // Get the form from the cache. + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // If $form cannot be loaded from the cache, the form_build_id must be + // invalid, which means that someone performed a POST request onto + // system/ajax without actually viewing the concerned form in the browser. + // This is likely a hacking attempt as it never happens under normal + // circumstances. + watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING); + throw new BadRequestHttpException(); + } + + // Since some of the submit handlers are run, redirects need to be disabled. + $form_state['no_redirect'] = TRUE; + + // When a form is rebuilt after Ajax processing, its #build_id and #action + // should not change. + // @see drupal_rebuild_form() + $form_state['rebuild_info']['copy']['#build_id'] = TRUE; + $form_state['rebuild_info']['copy']['#action'] = TRUE; + + // The form needs to be processed; prepare for that by setting a few internal + // variables. + $form_state['input'] = $request->request->all(); + $form_id = $form['#form_id']; + + return array($form, $form_state, $form_id, $form_build_id); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php index b464a46..2052c17 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php @@ -51,5 +51,19 @@ function testSimpleAjaxFormValue() { $expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item); $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.'); } + + // Verify that an AJAX element without a callback throws error code 500. + $edit = array( + 'select_empty_callback' => 'red', + ); + $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select_empty_callback'); + $this->assertResponse(500); + + // Verify that an AJAX element with a nonexistent callback throws error code 500. + $edit = array( + 'select_nonexistent_callback' => 'red', + ); + $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select_nonexistent_callback'); + $this->assertResponse(500); } } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 4a7cdf4..8286737 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -619,12 +619,9 @@ function system_menu() { ); $items['system/ajax'] = array( 'title' => 'AHAH callback', - 'page callback' => 'ajax_form_callback', - 'access callback' => TRUE, + 'route_name' => 'system.ajax', 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, - 'file path' => 'core/includes', - 'file' => 'form.inc', ); $items['admin'] = array( 'title' => 'Administration', diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 1adf224..61363d6 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -1,3 +1,9 @@ +system.ajax: + pattern: '/system/ajax' + defaults: + _controller: '\Drupal\system\Controller\FormAjaxController::content' + requirements: + _access: 'TRUE' system.cron: pattern: '/cron/{key}' defaults: diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module index 1682c01..a5d8e19 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module +++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module @@ -72,6 +72,19 @@ function ajax_forms_test_simple_form($form, &$form_state) { '#type' => 'submit', '#value' => t('submit'), ); + + $form['select_empty_callback'] = array( + '#type' => 'select', + '#options' => array('red' => 'red'), + '#ajax' => array('callback' => ''), + ); + + $form['select_nonexistent_callback'] = array( + '#type' => 'select', + '#options' => array('red' => 'red'), + '#ajax' => array('callback' => 'some_function_that_does_not_exist'), + ); + return $form; } diff --git a/core/tests/Drupal/Tests/Core/HttpKernelTest.php b/core/tests/Drupal/Tests/Core/HttpKernelTest.php deleted file mode 100644 index b51e108..0000000 --- a/core/tests/Drupal/Tests/Core/HttpKernelTest.php +++ /dev/null @@ -1,65 +0,0 @@ - 'HttpKernel (Unit)', - 'description' => 'Tests the HttpKernel.', - 'group' => 'Routing', - ); - } - - /** - * Tests the forward method. - * - * @see \Drupal\Core\HttpKernel::setupSubrequest() - */ - public function testSetupSubrequest() { - $container = new ContainerBuilder(); - - $request = new Request(); - $container->addScope(new Scope('request')); - $container->enterScope('request'); - $container->set('request', $request, 'request'); - - $dispatcher = new EventDispatcher(); - $controller_resolver = new ControllerResolver($container); - - $http_kernel = new HttpKernel($dispatcher, $container, $controller_resolver); - - $test_controller = '\Drupal\Tests\Core\Controller\TestController'; - $random_attribute = $this->randomName(); - $subrequest = $http_kernel->setupSubrequest($test_controller, array('custom_attribute' => $random_attribute), array('custom_query' => $random_attribute)); - $this->assertNotSame($subrequest, $request, 'The subrequest is not the same as the main request.'); - $this->assertEquals($subrequest->attributes->get('custom_attribute'), $random_attribute, 'Attributes are set from the subrequest.'); - $this->assertEquals($subrequest->query->get('custom_query'), $random_attribute, 'Query attributes are set from the subrequest.'); - $this->assertEquals($subrequest->attributes->get('_controller'), $test_controller, 'Controller attribute got set.'); - - $subrequest = $http_kernel->setupSubrequest(NULL, array(), array()); - $this->assertFalse($subrequest->attributes->has('_controller'), 'Ensure that _controller is not copied when no controller was set before.'); - } - -}