diff --git a/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php b/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php new file mode 100644 index 0000000..f33b9b0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/EventSubscriber/RouteSubscriber.php @@ -0,0 +1,43 @@ +getRouteCollection(); + + $views = views_get_applicable_views('uses_hook_menu'); + foreach ($views as $data) { + list($view, $display_id) = $data; + $view->collectRoutes($display_id, $collection); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php index bb3fbdf..96a0070 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php @@ -9,6 +9,7 @@ use Drupal\views\ViewExecutable; use \Drupal\views\Plugin\views\PluginBase; +use Symfony\Component\Routing\RouteCollection; /** * @defgroup views_display_plugins Views display plugins @@ -2408,6 +2409,17 @@ public function renderMoreLink() { } /** + * Adds the route entry of a view to the collection.# + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * A collection of routes that should be registered for this resource. + */ + public function collectRoutes(RouteCollection $collection) { + + } + + + /** * If this display creates a page with a menu item, implement it here. * * @param array $callbacks diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php index f2846c3..19d4dd0 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php @@ -92,7 +92,10 @@ public function execute() { // And the title, which is much easier. drupal_set_title(filter_xss_admin($this->view->getTitle()), PASS_THROUGH); - return $render; + $response = $this->view->getResponse(); + $response->setContent(drupal_render_page($render)); + + return $response; } /** diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php index 8aa9368..569a857 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php @@ -9,6 +9,8 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * The base display plugin for path/callbacks. This is used for pages and feeds. @@ -33,6 +35,50 @@ protected function defineOptions() { } /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::collectRoutes(). + */ + public function collectRoutes(RouteCollection $collection) { + $view_id = $this->view->storage->id(); + $display_id = $this->display['id']; + + // @todo How do we apply argument validation? + $bits = explode('/', $this->getOption('path')); + // @todo Figure out validation/argument loading. + // Replace % with %views_arg for menu autoloading and add to the + // page arguments so the argument actually comes through. + $arg_counter = 0; + foreach ($bits as $pos => &$bit) { + if ($bit == '%') { + $bit = '{arg' . $arg_counter++ . '}'; + } + } + $route_path = '/' . implode('/', $bits); + + $route = new Route($route_path, array( + '_controller' => 'views.page_controller:handle', + 'view_id' => $view_id, + 'display_id' => $display_id, + )); + + // Add access check parameters to the route. + $access_plugin = $this->getPlugin('access'); + if (!isset($access_plugin)) { + // @todo Do we want to support a default plugin in getPlugin itself? + $access_plugin = drupal_container()->get('plugin.manager.views.access')->createInstance('none'); + } + $callback = $access_plugin->get_access_callback(); + if (is_array($callback)) { + list($access_callback, $args) = $callback; + $route->addOptions(array( + '_access_callback' => $access_callback, + '_access_arguments' => $args, + )); + } + + $collection->add("$view_id.$display_id", $route); + } + + /** * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::executeHookMenu(). */ public function executeHookMenu($callbacks) { @@ -97,8 +143,10 @@ public function executeHookMenu($callbacks) { if ($path) { $items[$path] = array( // Default views page entry. - 'page callback' => 'views_page', - 'page arguments' => $page_arguments, + // @todo _menu_router_build() denies access to paths without a page + // callback. + 'page callback' => 'NOT_USED', + 'page arguments' => array(), // Default access check (per display). 'access callback' => 'views_access', 'access arguments' => $access_arguments, @@ -158,8 +206,10 @@ public function executeHookMenu($callbacks) { $default_path = implode('/', $bits); $items[$default_path] = array( // Default views page entry. - 'page callback' => 'views_page', - 'page arguments' => $page_arguments, + // @todo _menu_router_build() denies access to paths without a page + // callback. + 'page callback' => 'NOT_USED', + 'page arguments' => array(), // Default access check (per display). 'access callback' => 'views_access', 'access arguments' => $access_arguments, diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php index 49ed01c..3f36324 100644 --- a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php @@ -7,14 +7,16 @@ namespace Drupal\views\Tests\Plugin; -use Drupal\views\Tests\Plugin\PluginTestBase; +use Drupal\views\Tests\ViewUnitTestBase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Tests the page display plugin. * * @see Drupal\views\Plugin\display\Page */ -class DisplayPageTest extends PluginTestBase { +class DisplayPageTest extends ViewUnitTestBase { /** * Views used by this test. @@ -23,6 +25,20 @@ class DisplayPageTest extends PluginTestBase { */ public static $testViews = array('test_page_display'); + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('system'); + + /** + * The router dumper to get all routes. + * + * @var \Drupal\Core\Routing\MatcherDumper + */ + protected $routerDumper; + public static function getInfo() { return array( 'name' => 'Display: Page plugin', @@ -34,18 +50,37 @@ public static function getInfo() { protected function setUp() { parent::setUp(); - $this->enableViewsTestModule(); + // Setup the needed tables in order to make the drupal router working. + $this->installSchema('system', 'router'); + $this->installSchema('system', 'url_alias'); + $this->installSchema('system', 'menu_router'); } /** * Checks the behavior of the page for access denied/not found behaviours. */ public function testPageResponses() { - $view = views_get_view('test_page_display'); - $this->drupalGet('test_page_display_403'); - $this->assertResponse(403); - $this->drupalGet('test_page_display_404'); - $this->assertResponse(404); + // @todo Importing a route should fire a container rebuild. + $this->container->get('router.builder')->rebuild(); + + $subrequest = Request::create('/test_page_display_403', 'GET'); + $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $this->assertEqual($response->getStatusCode(), 403); + + $subrequest = Request::create('/test_page_display_404', 'GET'); + $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $this->assertEqual($response->getStatusCode(), 404); + + $subrequest = Request::create('/test_page_display_200', 'GET'); + $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $this->assertEqual($response->getStatusCode(), 200); + } + + /** + * Checks that the router items are properly registered + */ + public function testPageRouterItems() { + $this->container->get('router.builder')->rebuild(); } } diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php new file mode 100644 index 0000000..e246ee7 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php @@ -0,0 +1,75 @@ + 'View page controller test', + 'description' => 'Tests views page controller.', + 'group' => 'Views' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->pageController = $this->container->get('views.page_controller'); + } + + + /** + * Tests the page controller. + */ + public function testPageController() { + $this->assertTrue($this->pageController instanceof ViewPageController, 'Ensure the right class is stored in the container'); + + // Pass in a non existent view. + $random_view_id = $this->randomName(); + $exception_found = FALSE; + try { + $this->pageController->handle($random_view_id, 'default'); + } + + catch (\Exception $e) { + $exception_found = TRUE; + } + $this->assertTrue($exception_found, 'Exception thrown when view was not found'); + + $output = $this->pageController->handle('test_page_view', 'default'); + $this->assertTrue(is_array($output)); + $this->assertEqual($output['#view']->storage->id, 'test_page_view', 'The right view was executed.'); + + $output = $this->pageController->handle('test_page_view', 'page_1'); + $this->assertTrue($output instanceof Response, 'Ensure the page display returns a response object.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php index 334b5da..158697c 100644 --- a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php +++ b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php @@ -24,6 +24,16 @@ */ abstract class ViewUnitTestBase extends DrupalUnitTestBase { + /** + * Overrides \Drupal\simpletest\DrupalUnitTestBase::containerBuild(). + */ + public function containerBuild($container) { + parent::containerBuild($container); + + // @todo This mock router is not usable yet. + // $container->register('router.route_provider', 'Drupal\system\Tests\Routing\MockRouteProvider'); + } + protected function setUp() { parent::setUp(); diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php index 898503f..cbb497c 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\Response; use Drupal\views\Plugin\Core\Entity\View; +use Symfony\Component\Routing\RouteCollection; /** * @defgroup views_objects Objects that represent a View or part of a view @@ -1463,6 +1464,29 @@ public function attachDisplays() { } /** + * Called to collect router entries from the view and the display_handler. + * + * @param string $display_id + * A display id. + * @param \Symfony\Component\Routing\RouteCollection $collection + * A menu callback array passed from views_menu_alter(). + * + * @return bool + * Returns FALSE if the display has not been found, TRUE otherwise. + */ + public function collectRoutes($display_id = NULL, RouteCollection $collection = NULL) { + // This was probably already called, but it's good to be safe. + if (!$this->setDisplay($display_id)) { + return FALSE; + } + + if (isset($this->display_handler)) { + $this->display_handler->collectRoutes($collection); + return TRUE; + } + } + + /** * Called to get hook_menu() information from the view and the named display handler. * * @param $display_id diff --git a/core/modules/views/lib/Drupal/views/ViewPageController.php b/core/modules/views/lib/Drupal/views/ViewPageController.php new file mode 100644 index 0000000..4360b35 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewPageController.php @@ -0,0 +1,61 @@ +entityManager = $entity_manager; + } + + /** + * Handles a response for a view. + * + * @param string $view_id + * The ID of the view to execute. + * @param string $display_id + * The ID of the display to execute. + */ + public function handle(Request $request) { + $view_id = $request->attributes->get('view_id'); + $display_id = $request->attributes->get('display_id'); + + $entities = $this->entityManager->getStorageController('view')->load(array($view_id)); + $entity = reset($entities); + if (empty($entity)) { + throw new \Exception(format_string('Page controller for view %id requested, but view was not found.', array('%id' => $view_id))); + } + $view = $entity->get('executable'); + + $args = array(); + foreach ($request->attributes->all() as $key => $argument) { + if (strpos($key, 'arg') !== FALSE) { + $args[] = $argument; + } + } + + return $view->executeDisplay($display_id, $args); + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php b/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php new file mode 100644 index 0000000..22df622 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php @@ -0,0 +1,40 @@ +getDefault('view_id') && $route->getDefault('display_id'); + } + + /** + * Implements \Drupal\Core\Access\AccessCheckInterface::applies(). + */ + public function access(Route $route, Request $request) { + $access_callback = $route->getOption('_access_callback'); + $access_arguments = $route->getOption('_access_arguments'); + + if (is_callable($access_callback)) { + return call_user_func_array($access_callback, $access_arguments); + } + // If there is no access check for the view, allow it. + return TRUE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewsBundle.php b/core/modules/views/lib/Drupal/views/ViewsBundle.php index dfb6dd6..1a737b5 100644 --- a/core/modules/views/lib/Drupal/views/ViewsBundle.php +++ b/core/modules/views/lib/Drupal/views/ViewsBundle.php @@ -37,6 +37,16 @@ public function build(ContainerBuilder $container) { ->addArgument(new Reference('config.factory')); $container->register('views.executable', 'Drupal\views\ViewExecutableFactory'); + + $container->register('views.page_controller', 'Drupal\views\ViewPageController') + ->addArgument(new Reference('plugin.manager.entity')); + + $container->register('views.route_subscriber', 'Drupal\views\EventSubscriber\RouteSubscriber') + ->addArgument(new Reference('config.factory')) + ->addTag('event_subscriber'); + + $container->register('views.route_access_check', 'Drupal\views\ViewsAccessCheck') + ->addTag('access_check'); } } diff --git a/core/modules/views/tests/views_test_config/test_views/views.view.test_page_display.yml b/core/modules/views/tests/views_test_config/test_views/views.view.test_page_display.yml index 3256666..12c5b2d 100644 --- a/core/modules/views/tests/views_test_config/test_views/views.view.test_page_display.yml +++ b/core/modules/views/tests/views_test_config/test_views/views.view.test_page_display.yml @@ -5,10 +5,18 @@ status: '1' display: default: display_options: - access: - type: none - cache: - type: none + defaults: + fields: '0' + pager: '0' + pager_options: '0' + sorts: '0' + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + plugin_id: numeric display_plugin: default display_title: Master id: default @@ -27,6 +35,13 @@ display: display_title: Page id: page_2 position: '0' + page_3: + display_options: + path: test_page_display_200 + display_plugin: page + display_title: Page + id: page_3 + position: '0' human_name: '' id: test_page_display tag: '' diff --git a/core/modules/views/tests/views_test_config/test_views/views.view.test_page_view.yml b/core/modules/views/tests/views_test_config/test_views/views.view.test_page_view.yml new file mode 100644 index 0000000..41d09e5 --- /dev/null +++ b/core/modules/views/tests/views_test_config/test_views/views.view.test_page_view.yml @@ -0,0 +1,30 @@ +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + defaults: + fields: '0' + pager: '0' + pager_options: '0' + sorts: '0' + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + plugin_id: numeric + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_plugin: page + display_title: Test page view + id: page_1 +human_name: '' +id: test_page_view +tag: '' diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 8ec5d5e..99cd4cb 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -695,6 +695,12 @@ function views_invalidate_cache() { // Set the menu as needed to be rebuilt. state()->set('menu_rebuild_needed', TRUE); + // Set the router to be rebuild. + // @todo This isn't a good fix. + if (db_table_exists('router')) { + drupal_container()->get('router.builder')->rebuild(); + } + // Invalidate the block cache to update views block derivatives. if (module_exists('block')) { drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();