diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 2a72267..cdaf107 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -26,7 +26,8 @@ class AjaxResponse extends JsonResponse { * Add an AJAX command to the response. * * @param object $command - * An AJAX command object implementing CommandInterface. + * An AJAX command object implementing CommandInterface or a rendered + * command (an array). * @param boolean $prepend * A boolean which determines whether the new command should be executed * before previously added commands. Defaults to FALSE. @@ -35,11 +36,14 @@ class AjaxResponse extends JsonResponse { * The current AjaxResponse. */ public function addCommand($command, $prepend = FALSE) { + // @todo Remove support for already rendered commands once all core ajax + // callbacks are are converted. + $rendered_command = is_array($command) ? $command : $command->render(); if ($prepend) { - array_unshift($this->commands, $command->render()); + array_unshift($this->commands, $rendered_command); } else { - $this->commands[] = $command->render(); + $this->commands[] = $rendered_command; } return $this; diff --git a/core/lib/Drupal/Core/AjaxController.php b/core/lib/Drupal/Core/AjaxController.php index 31259fb..c423489 100644 --- a/core/lib/Drupal/Core/AjaxController.php +++ b/core/lib/Drupal/Core/AjaxController.php @@ -12,8 +12,6 @@ use Drupal\Core\Ajax\PrependCommand; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Default controller for ajax requests. @@ -56,7 +54,18 @@ public function content(Request $request, $_content) { if ($response->isOk()) { // If there is already an AjaxResponse, then return it without // manipulation. - if (!($response instanceof AjaxResponse)) { + if ($response instanceof AjaxResponse) { + return $response; + } + elseif (is_array($response)) { + // Add all commands to the ajax response. + $ajax_response = new AjaxResponse(); + foreach ($response['#commands'] as $command) { + $ajax_response->addCommand($command); + } + $response = $ajax_response; + } + else { // Pull the content out of the response. $content = $response->getContent(); // A page callback could return a render array or a string. diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index ee63bfa..5d3839c 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -230,15 +230,10 @@ public function build(ContainerBuilder $container) { $container->register('mime_type_matcher', 'Drupal\Core\Routing\MimeTypeMatcher') ->addTag('route_filter'); - $container->register('paramconverter_manager', 'Drupal\Core\ParamConverter\ParamConverterManager') - ->addTag('route_enhancer'); $container->register('paramconverter.entity', 'Drupal\Core\ParamConverter\EntityConverter') ->addArgument(new Reference('plugin.manager.entity')) ->addTag('paramconverter'); - $container->register('router_processor_subscriber', 'Drupal\Core\EventSubscriber\RouteProcessorSubscriber') - ->addArgument(new Reference('content_negotiation')) - ->addTag('event_subscriber'); $container->register('router_listener', 'Symfony\Component\HttpKernel\EventListener\RouterListener') ->addArgument(new Reference('router')) ->addTag('event_subscriber'); @@ -322,7 +317,6 @@ public function build(ContainerBuilder $container) { $container->addCompilerPass(new RegisterAccessChecksPass()); // Add a compiler pass for upcasting of entity route parameters. $container->addCompilerPass(new RegisterParamConvertersPass()); - $container->addCompilerPass(new RegisterRouteEnhancersPass()); // Add a compiler pass for registering services needing destruction. $container->addCompilerPass(new RegisterServicesForDestructionPass()); } @@ -380,6 +374,23 @@ protected function registerRouting(ContainerBuilder $container) { ->addMethodCall('setContext', array(new Reference('router.request_context'))) ->addMethodCall('add', array(new Reference('router.dynamic'))) ->addMethodCall('add', array(new Reference('legacy_router'))); + + // Add a route enhancer to upcast parameters to objects if possible. + $container->register('paramconverter_manager', 'Drupal\Core\ParamConverter\ParamConverterManager') + ->addTag('route_enhancer', array('priority' => 50)); + + // Add core route enhancers to dynamically derive the _controller + $container->register('route_enhancer.ajax', 'Drupal\Core\Routing\Enhancer\AjaxEnhancer') + ->addArgument(new Reference('content_negotiation')) + ->addTag('route_enhancer', array('priority' => 20)); + $container->register('route_enhancer.form', 'Drupal\Core\Routing\Enhancer\FormEnhancer') + ->addArgument(new Reference('content_negotiation')) + ->addTag('route_enhancer', array('priority' => 10)); + $container->register('route_enhancer.page', 'Drupal\Core\Routing\Enhancer\PageEnhancer') + ->addArgument(new Reference('content_negotiation')) + ->addTag('route_enhancer', array('priority' => 0)); + + $container->addCompilerPass(new RegisterRouteEnhancersPass()); } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php deleted file mode 100644 index 7b1e34a..0000000 --- a/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php +++ /dev/null @@ -1,72 +0,0 @@ -negotiation = $negotiation; - } - - /** - * Sets a default controller for a route if one was not specified. - * - * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event - * Event that is created to create a response for a request. - */ - public function onRequestSetController(GetResponseEvent $event) { - $request = $event->getRequest(); - - // @todo This should all get converted into one or more RouteEnhancers. - if (!$request->attributes->has('_content') && $this->negotiation->getContentType($request) == 'drupal_ajax') { - $request->attributes->set('_content', $request->attributes->get('_controller')); - $request->attributes->set('_controller', '\Drupal\Core\AjaxController::content'); - } - elseif (!$request->attributes->has('_controller') && $this->negotiation->getContentType($request) === 'html') { - if ($request->attributes->has('_form')) { - $request->attributes->set('_controller', '\Drupal\Core\HtmlFormController::content'); - } - elseif ($request->attributes->has('_content')) { - $request->attributes->set('_controller', '\Drupal\Core\HtmlPageController::content'); - } - else { - throw new \InvalidArgumentException('No valid subcontroller key was found'); - } - } - } - - /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. - */ - static function getSubscribedEvents() { - // The RouterListener has priority 32, and we need to run after that. - $events[KernelEvents::REQUEST][] = array('onRequestSetController', 30); - - return $events; - } - -} diff --git a/core/lib/Drupal/Core/Form/FormAjaxController.php b/core/lib/Drupal/Core/Form/FormAjaxController.php new file mode 100644 index 0000000..c2c23fd --- /dev/null +++ b/core/lib/Drupal/Core/Form/FormAjaxController.php @@ -0,0 +1,50 @@ +negotiation = $negotiation; + } + + /** + * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance() + */ + public function enhance(array $defaults, Request $request) { + // Old-style routes work differently, since they are their own controller. + if ($request->attributes->get('_legacy') == TRUE) { + if (empty($defaults['_content']) && $this->negotiation->getContentType($request) == 'drupal_ajax') { + $defaults['_content'] = $defaults['_controller']; + $defaults['_controller'] = '\Drupal\Core\AjaxController::content'; + } + } + elseif (!empty($defaults['_controller']) && empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'drupal_ajax') { + $defaults['_content'] = $defaults['_controller']; + $defaults['_controller'] = '\Drupal\Core\AjaxController::content'; + } + else { + if (empty($defaults['_controller']) && !empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'drupal_ajax') { + $defaults['_controller'] = '\Drupal\Core\AjaxController::content'; + } + } + return $defaults; + } + +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php new file mode 100644 index 0000000..2210d03 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php @@ -0,0 +1,43 @@ +negotiation = $negotiation; + } + + /** + * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance() + */ + public function enhance(array $defaults, Request $request) { + if (empty($defaults['_controller']) && !empty($defaults['_form']) && $this->negotiation->getContentType($request) === 'html') { + $defaults['_controller'] = '\Drupal\Core\HtmlFormController::content'; + } + return $defaults; + } + +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php new file mode 100644 index 0000000..3d73e9e --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php @@ -0,0 +1,43 @@ +negotiation = $negotiation; + } + + /** + * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance() + */ + public function enhance(array $defaults, Request $request) { + if (empty($defaults['_controller']) && !empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'html') { + $defaults['_controller'] = '\Drupal\Core\HtmlPageController::content'; + } + return $defaults; + } + +} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 5cca38a..e3d2274 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1360,7 +1360,8 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a * (optional) Options to be forwarded to url(). * @param $headers * (optional) An array containing additional HTTP request headers, each - * formatted as "name: value". Forwarded to drupalPost(). + * formatted as "name: value". Forwarded to drupalPost(). Defaults to + * array('Accept: application/vnd.drupal-ajax'). * @param $form_html_id * (optional) HTML ID of the form to be submitted, use when there is more * than one identical form on the same page and the value of the triggering @@ -1377,6 +1378,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a * @see ajax.js */ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { + $headers += array('Accept: application/vnd.drupal-ajax'); // Get the content of the initial page prior to calling drupalPost(), since // drupalPost() replaces $this->content. if (isset($path)) { @@ -1411,7 +1413,7 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path } $ajax_html_ids = array(); foreach ($this->xpath('//*[@id]') as $element) { - $ajax_html_ids[] = (string) $element['id']; + $ajax_html_ids[] = (string) $element['id']; } if (!empty($ajax_html_ids)) { $extra_post .= '&' . urlencode('ajax_html_ids') . '=' . urlencode(implode(' ', $ajax_html_ids)); diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php index 8a3f45f..352e89f 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php @@ -106,4 +106,35 @@ public function testDynamicRoutes() { $this->assertResponse(200); $this->assertRaw('test5', 'The correct string was returned because the route was successful.'); } + + /** + * Checks that a request with text/html response gets rendered as a page. + */ + public function testControllerResolutionPage() { + $this->drupalGet('/router_test/test10'); + + $this->assertRaw('abcde', 'Correct body was found.'); + + // Confirm that the page wrapping is being added, so we're not getting a + // raw body returned. + $this->assertRaw('', 'Page markup was found.'); + + // In some instances, the subrequest handling may get confused and render + // a page inception style. This test verifies that is not happening. + $this->assertNoPattern('#.*#s', 'There was no double-page effect from a misrendered subrequest.'); + } + + /** + * Checks that an ajax request gets rendered as an Ajax response, by mime. + */ + public function testControllerResolutionAjax() { + // This will fail with a JSON parse error if the request is not routed to + // The correct controller. + $this->drupalGetAJAX('/router_test/test10', array(), array('Accept: application/vnd.drupal-ajax')); + + $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/json', 'Correct mime content type was returned'); + + $this->assertRaw('abcde', 'Correct body was found.'); + } + } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c4304f4..44ab0a8 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -627,15 +627,6 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); - $items['system/ajax'] = array( - 'title' => 'AHAH callback', - 'page callback' => 'ajax_form_callback', - 'access callback' => TRUE, - 'theme callback' => 'ajax_base_page_theme', - 'type' => MENU_CALLBACK, - 'file path' => 'core/includes', - 'file' => 'form.inc', - ); $items['system/timezone'] = array( 'title' => 'Time zone', 'page callback' => 'system_timezone', diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 085895e..a2a641e 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -4,3 +4,10 @@ system.cron: _controller: '\Drupal\system\CronController::run' requirements: _access_system_cron: 'TRUE' + +system.form_ajax: + pattern: '/system/ajax' + defaults: + _controller: '\Drupal\Core\Form\FormAjaxController::content' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php new file mode 100644 index 0000000..11fc1b0 --- /dev/null +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php @@ -0,0 +1,19 @@ +