Commit 04f662ff authored by webchick's avatar webchick

Issue #2027115 by dawehner, tim.plunkett: Allow views to override existing routing items.

parent d48480f3
......@@ -71,7 +71,7 @@ public function onRoutingRouteAlterSetAccessCheck(RouteBuildEvent $event) {
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestAccessCheck', 30);
// Setting very low priority to ensure access checks are run after alters.
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetAccessCheck', 0);
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetAccessCheck', -50);
return $events;
}
......
......@@ -124,7 +124,7 @@ public static function getFit($path) {
* @return string
* The path string, stripped of placeholders that have default values.
*/
protected static function getPathWithoutDefaults(Route $route) {
public static function getPathWithoutDefaults(Route $route) {
$path = $route->getPath();
$defaults = $route->getDefaults();
......
......@@ -8,6 +8,8 @@
namespace Drupal\rest\Plugin\views\display;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\views\Annotation\ViewsDisplay;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\ContentNegotiation;
......@@ -95,13 +97,17 @@ class RestExport extends PathPluginBase {
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state key value store.
* @param \Drupal\Core\ContentNegotiation $content_negotiation
* The content negotiation library.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ContentNegotiation $content_negotiation, Request $request) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
public function __construct(array $configuration, $plugin_id, array $plugin_definition, RouteProviderInterface $route_provider, KeyValueStoreInterface $state, ContentNegotiation $content_negotiation, Request $request) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->contentNegotiation = $content_negotiation;
$this->request = $request;
}
......@@ -114,6 +120,8 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider'),
$container->get('state'),
$container->get('content_negotiation'),
$container->get('request')
);
......
......@@ -77,6 +77,14 @@ protected function setUp() {
->getMock();
$container->set('plugin.manager.views.access', $access_manager);
$route_provider = $this->getMockBuilder('\Drupal\Core\Routing\RouteProviderInterface')
->disableOriginalConstructor()
->getMock();
$container->set('router.route_provider', $route_provider);
$state = $this->getMock('\Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$container->set('state', $state);
$style_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
->disableOriginalConstructor()
->getMock();
......
<?php
/**
* @file
* Contains \Drupal\user\Controller\UserAdmin.
*/
namespace Drupal\user\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\user\UserStorageControllerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a user administrative listing.
*
* @todo Convert this to a entity list controller once table sort is supported.
*/
class UserAdmin extends ControllerBase implements ContainerInjectionInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The user storage controller.
*
* @var \Drupal\user\UserStorageControllerInterface
*/
protected $storageController;
/**
* The entity query.
*
* @var \Drupal\Core\Entity\Query\QueryInterface
*/
protected $entityQuery;
/**
* Constructs a new UserAdmin object.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\user\UserStorageControllerInterface $storage_controller
* The user storage controller.
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
* The entity query.
*/
public function __construct(Connection $connection, ModuleHandlerInterface $module_handler, UserStorageControllerInterface $storage_controller, QueryInterface $entity_query) {
$this->connection = $connection;
$this->moduleHandler = $module_handler;
$this->storageController = $storage_controller;
$this->entityQuery = $entity_query;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('module_handler'),
$container->get('entity.manager')->getStorageController('user'),
$container->get('entity.query')->get('user')
);
}
/**
* User administrative listing.
*
* @return array
* A render array as expected by drupal_render().
*/
public function userList() {
$header = array(
'username' => array('data' => $this->t('Username'), 'field' => 'name', 'specifier' => 'name'),
'status' => array('data' => $this->t('Status'), 'field' => 'status', 'specifier' => 'status', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'roles' => array('data' => $this->t('Roles'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'member_for' => array('data' => $this->t('Member for'), 'field' => 'created', 'specifier' => 'created', 'sort' => 'desc', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'access' => array('data' => $this->t('Last access'), 'field' => 'access', 'specifier' => 'access', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'operations' => $this->t('Operations'),
);
$this->entityQuery->condition('uid', 0, '<>');
$this->entityQuery->pager(50);
$this->entityQuery->tableSort($header);
$uids = $this->entityQuery->execute();
$accounts = $this->storageController->loadMultiple($uids);
$destination = drupal_get_destination();
$status = array($this->t('blocked'), $this->t('active'));
$roles = array_map('\Drupal\Component\Utility\String::checkPlain', user_role_names(TRUE));
unset($roles[DRUPAL_AUTHENTICATED_RID]);
$options = array();
foreach ($accounts as $account) {
$users_roles = array();
foreach ($account->getRoles() as $role) {
if (isset($roles[$role])) {
$users_roles[] = $roles[$role];
}
}
asort($users_roles);
$options[$account->id()]['username']['data'] = array(
'#theme' => 'username',
'#account' => $account,
);
$options[$account->id()]['status'] = $status[$account->isActive()];
$options[$account->id()]['roles']['data'] = array(
'#theme' => 'item_list',
'#items' => $users_roles,
);
$options[$account->id()]['member_for'] = format_interval(REQUEST_TIME - $account->getCreatedTime());
$options[$account->id()]['access'] = $account->access ? $this->t('@time ago', array('@time' => format_interval(REQUEST_TIME - $account->getLastAccessedTime()))) : t('never');
$links = array();
$links['edit'] = array(
'title' => $this->t('Edit'),
'href' => 'user/' . $account->id() . '/edit',
'query' => $destination,
);
if ($this->moduleHandler->invoke('content_translation', 'translate_access', array($account))) {
$links['translate'] = array(
'title' => $this->t('Translate'),
'href' => 'user/' . $account->id() . '/translations',
'query' => $destination,
);
}
$options[$account->id()]['operations']['data'] = array(
'#type' => 'operations',
'#links' => $links,
);
}
$build['accounts'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $options,
'#empty' => $this->t('No people available.'),
);
$build['pager'] = array(
'#theme' =>'pager',
);
return $build;
}
}
......@@ -5,91 +5,6 @@
* Admin page callback file for the user module.
*/
/**
* Page callback: User administration page.
*/
function user_admin_account() {
$header = array(
'username' => array('data' => t('Username'), 'field' => 'u.name'),
'status' => array('data' => t('Status'), 'field' => 'u.status', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'roles' => array('data' => t('Roles'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'member_for' => array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'access' => array('data' => t('Last access'), 'field' => 'u.access', 'class' => array(RESPONSIVE_PRIORITY_LOW)),
'operations' => t('Operations'),
);
$query = db_select('users', 'u');
$query->condition('u.uid', 0, '<>');
$count_query = clone $query;
$count_query->addExpression('COUNT(u.uid)');
$query = $query
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->extend('Drupal\Core\Database\Query\TableSortExtender');
$query
->fields('u', array('uid'))
->limit(50)
->orderByHeader($header)
->setCountQuery($count_query);
$uids = $query->execute()
->fetchCol('uid');
$destination = drupal_get_destination();
$accounts = user_load_multiple($uids);
foreach ($accounts as $account) {
$users_roles = $account->getRoles();
unset($users_roles[0]);
asort($users_roles);
$username = array(
'#theme' => 'username',
'#account' => $account,
);
$item_list = array(
'#theme' => 'item_list',
'#items' => $users_roles,
);
$options[$account->id()] = array(
'username' => drupal_render($username),
'status' => $account->isActive() ? t('active') : t('blocked'),
'roles' => drupal_render($item_list),
'member_for' => format_interval(REQUEST_TIME - $account->getCreatedTime()),
'access' => $account->getLastAccessedTime() ? t('@time ago', array('@time' => format_interval(REQUEST_TIME - $account->getLastAccessedTime()))) : t('never'),
);
$links = array();
$links['edit'] = array(
'title' => t('Edit'),
'href' => 'user/' . $account->id() . '/edit',
'query' => $destination,
);
if (module_invoke('content_translation', 'translate_access', $account)) {
$links['translate'] = array(
'title' => t('Translate'),
'href' => 'user/' . $account->id() . '/translations',
'query' => $destination,
);
}
$options[$account->id()]['operations']['data'] = array(
'#type' => 'operations',
'#links' => $links,
);
}
$form['accounts'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $options,
'#empty' => t('No people available.'),
);
$form['pager'] = array(
'#theme' =>'pager',
);
return $form;
}
/**
* Returns HTML for an individual permission description.
*
......
user_admin_create:
route_name: user.admin_create
title: 'Add user'
appears_on:
- user.admin_account
......@@ -778,11 +778,13 @@ function user_menu() {
$items['admin/people'] = array(
'title' => 'People',
'description' => 'Manage user accounts, roles, and permissions.',
'page callback' => 'user_admin_account',
'access arguments' => array('administer users'),
'route_name' => 'user.admin_account',
'position' => 'left',
'weight' => -4,
'file' => 'user.admin.inc',
);
$items['admin/people/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
// Permissions and role forms.
$items['admin/people/permissions'] = array(
......@@ -812,12 +814,6 @@ function user_menu() {
'context' => MENU_CONTEXT_INLINE,
);
$items['admin/people/create'] = array(
'title' => 'Add user',
'route_name' => 'user.admin_create',
'type' => MENU_LOCAL_ACTION,
);
// Administration pages.
$items['admin/config/people'] = array(
'title' => 'People',
......
......@@ -40,6 +40,13 @@ user.account_settings:
requirements:
_permission: 'administer account settings'
user.admin_account:
path: '/admin/people'
defaults:
_controller: '\Drupal\user\Controller\UserAdmin::userList'
requirements:
_permission: 'administer users'
user.admin_create:
path: '/admin/people/create'
defaults:
......
......@@ -7,24 +7,104 @@
namespace Drupal\views\EventSubscriber;
use Drupal\Component\Utility\MapArray;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\views\Plugin\views\display\DisplayRouterInterface;
use Drupal\views\ViewExecutable;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Builds up the routes of all views.
*
* The general idea is to execute first all alter hooks to determine which
* routes are overridden by views. This information is used to determine which
* views have to be added by views in the dynamic event.
*
* @see \Drupal\views\Plugin\views\display\PathPluginBase
*/
class RouteSubscriber implements EventSubscriberInterface {
class RouteSubscriber implements EventSubscriberInterface, DestructableInterface {
/**
* Stores a list of view,display IDs which haven't be used in the alter event.
*
* @var array
*/
protected $viewsDisplayPairs;
/**
* The view storage controller.
*
* @var \Drupal\Core\Entity\EntityStorageControllerInterface
*/
protected $viewStorageController;
/**
* The state key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $state;
/**
* Stores an array of route names keyed by view_id.display_id.
*
* @var array
*/
protected $viewRouteNames = array();
/**
* Constructs a \Drupal\views\EventSubscriber\RouteSubscriber instance.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state key value store.
*/
public function __construct(EntityManager $entity_manager, KeyValueStoreInterface $state) {
$this->viewStorageController = $entity_manager->getStorageController('view');
$this->state = $state;
}
/**
* Resets the internal state of the route subscriber.
*/
public function reset() {
$this->viewsDisplayPairs = NULL;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
$events[RoutingEvents::ALTER] = 'alterRoutes';
return $events;
}
/**
* Gets all the views and display IDs using a route.
*/
protected function getViewsDisplayIDsWithRoute() {
if (!isset($this->viewsDisplayPairs)) {
$this->viewsDisplayPairs = array();
// @todo Convert this method to some service.
$views = $this->getApplicableViews();
foreach ($views as $data) {
list($view, $display_id) = $data;
$id = $view->storage->id();
$this->viewsDisplayPairs[] = $id . '.' . $display_id;
}
$this->viewsDisplayPairs = MapArray::copyValuesToKeys($this->viewsDisplayPairs);
}
return $this->viewsDisplayPairs;
}
/**
* Adds routes defined by all views.
*
......@@ -34,16 +114,67 @@ public static function getSubscribedEvents() {
public function dynamicRoutes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
$views = views_get_applicable_views('uses_route');
foreach ($views as $data) {
list($view, $display_id) = $data;
foreach ($this->getViewsDisplayIDsWithRoute() as $pair) {
list($view_id, $display_id) = explode('.', $pair);
$view = $this->viewStorageController->load($view_id);
// @todo This should have an executable factory injected.
if (($view = $view->getExecutable()) && $view instanceof ViewExecutable) {
if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) {
if ($display instanceof DisplayRouterInterface) {
$display->collectRoutes($collection);
$view_route_names = (array) $display->collectRoutes($collection);
$this->viewRouteNames += $view_route_names;
}
}
$view->destroy();
}
}
$this->state->set('views.view_route_names', $this->viewRouteNames);
}
/**
* Alters existing routes.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route building event.
*/
public function alterRoutes(RouteBuildEvent $event) {
foreach ($this->getViewsDisplayIDsWithRoute() as $pair) {
list($view_id, $display_id) = explode('.', $pair);
$view = $this->viewStorageController->load($view_id);
// @todo This should have an executable factory injected.
if (($view = $view->getExecutable()) && $view instanceof ViewExecutable) {
if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) {
if ($display instanceof DisplayRouterInterface) {
// If the display returns TRUE a route item was found, so it does not
// have to be added.
$view_route_names = $display->alterRoutes($event->getRouteCollection());
$this->viewRouteNames += $view_route_names;
foreach ($view_route_names as $id_display => $route_name) {
unset($this->viewsDisplayPairs[$id_display]);
}
}
}
$view->destroy();
}
}
}
/**
* {@inheritdoc}
*/
public function destruct() {
$this->state->set('views.view_route_names', $this->viewRouteNames);
}
/**
* Returns all views/display combinations with routes.
*
* @see views_get_applicable_views()
*/
protected function getApplicableViews() {
return views_get_applicable_views('uses_route');
}
}
......@@ -7,14 +7,14 @@
namespace Drupal\views\Plugin\views\display;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines an interface for displays that can collect routes.
*
* In addition to implementing the interface, specify 'uses_routes' in the
* plugin definition.
*/
use Symfony\Component\Routing\RouteCollection;
interface DisplayRouterInterface {
/**
......@@ -25,4 +25,17 @@ interface DisplayRouterInterface {
*/
public function collectRoutes(RouteCollection $collection);
/**
* Alters a collection of routes and replaces definitions to the view.
*
* Most of the collections won't have the needed route, so by the return value
* the method can specify to break the search.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
*
* @return array
* Returns a list of "$view_id.$display_id" elements which got overridden.
*/
public function alterRoutes(RouteCollection $collection);
}
......@@ -7,8 +7,12 @@
namespace Drupal\views\Plugin\views\display;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Routing\RouteCompiler;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Route;
......@@ -16,9 +20,59 @@
/**
* The base display plugin for path/callbacks. This is used for pages and feeds.
*
* @see \Drupal\views\EventSubscriber\RouteSubscriber
*/
abstract class PathPluginBase extends DisplayPluginBase implements DisplayRouterInterface {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The state key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $state;
/**
* Constructs a PathPluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state key value store.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, RouteProviderInterface $route_provider, KeyValueStoreInterface $state) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeProvider = $route_provider;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider'),
$container->get('state')
);
}
/**
* Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::hasPath().
*/
......@@ -60,12 +114,17 @@ protected function defineOptions() {
}
/**
* {@inheritdoc}
* Generates a route entry for a given view and display.
*
* @param string $view_id
* The ID of the view.
* @param string $display_id
* The current display ID.
*
* @return \Symfony\Component\Routing\Route
* The route for the view.
*/
public function collectRoutes(RouteCollection $collection) {
$view_id = $this->view->storage->id();
$display_id = $this->display['id'];
protected function getRoute($view_id, $display_id) {
$defaults = array(
'_controller' => 'Drupal\views\Routing\ViewPageController::handle',
'view_id' => $view_id,
......@@ -80,7 +139,7 @@ public function collectRoutes(RouteCollection $collection) {
$arg_counter = 0;
$this->view->initHandlers();
$view_arguments = $this->view->argument;
$view_arguments = (array) $this->view->argument;
$argument_ids = array_keys($view_arguments);
$total_arguments = count($argument_ids);
......@@ -126,8 +185,50 @@ public function collectRoutes(RouteCollection $collection) {
$access_plugin = Views::pluginManager('access')->createInstance('none');
}
$access_plugin->alterRouteDefinition($route);
return $route;
}