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) { ...@@ -71,7 +71,7 @@ public function onRoutingRouteAlterSetAccessCheck(RouteBuildEvent $event) {
static function getSubscribedEvents() { static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestAccessCheck', 30); $events[KernelEvents::REQUEST][] = array('onKernelRequestAccessCheck', 30);
// Setting very low priority to ensure access checks are run after alters. // 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; return $events;
} }
......
...@@ -124,7 +124,7 @@ public static function getFit($path) { ...@@ -124,7 +124,7 @@ public static function getFit($path) {
* @return string * @return string
* The path string, stripped of placeholders that have default values. * 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(); $path = $route->getPath();
$defaults = $route->getDefaults(); $defaults = $route->getDefaults();
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
namespace Drupal\rest\Plugin\views\display; namespace Drupal\rest\Plugin\views\display;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\views\Annotation\ViewsDisplay; use Drupal\views\Annotation\ViewsDisplay;
use Drupal\Core\Annotation\Translation; use Drupal\Core\Annotation\Translation;
use Drupal\Core\ContentNegotiation; use Drupal\Core\ContentNegotiation;
...@@ -95,13 +97,17 @@ class RestExport extends PathPluginBase { ...@@ -95,13 +97,17 @@ class RestExport extends PathPluginBase {
* The plugin_id for the plugin instance. * The plugin_id for the plugin instance.
* @param array $plugin_definition * @param array $plugin_definition
* The plugin implementation 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 * @param \Drupal\Core\ContentNegotiation $content_negotiation
* The content negotiation library. * The content negotiation library.
* @param \Symfony\Component\HttpFoundation\Request $request * @param \Symfony\Component\HttpFoundation\Request $request
* The request object. * The request object.
*/ */
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ContentNegotiation $content_negotiation, Request $request) { 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); parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->contentNegotiation = $content_negotiation; $this->contentNegotiation = $content_negotiation;
$this->request = $request; $this->request = $request;
} }
...@@ -114,6 +120,8 @@ public static function create(ContainerInterface $container, array $configuratio ...@@ -114,6 +120,8 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration, $configuration,
$plugin_id, $plugin_id,
$plugin_definition, $plugin_definition,
$container->get('router.route_provider'),
$container->get('state'),
$container->get('content_negotiation'), $container->get('content_negotiation'),
$container->get('request') $container->get('request')
); );
......
...@@ -77,6 +77,14 @@ protected function setUp() { ...@@ -77,6 +77,14 @@ protected function setUp() {
->getMock(); ->getMock();
$container->set('plugin.manager.views.access', $access_manager); $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') $style_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->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 @@ ...@@ -5,91 +5,6 @@
* Admin page callback file for the user module. * 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. * 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() { ...@@ -778,11 +778,13 @@ function user_menu() {
$items['admin/people'] = array( $items['admin/people'] = array(
'title' => 'People', 'title' => 'People',
'description' => 'Manage user accounts, roles, and permissions.', 'description' => 'Manage user accounts, roles, and permissions.',
'page callback' => 'user_admin_account', 'route_name' => 'user.admin_account',
'access arguments' => array('administer users'),
'position' => 'left', 'position' => 'left',
'weight' => -4, 'weight' => -4,
'file' => 'user.admin.inc', );
$items['admin/people/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
); );
// Permissions and role forms. // Permissions and role forms.
$items['admin/people/permissions'] = array( $items['admin/people/permissions'] = array(
...@@ -812,12 +814,6 @@ function user_menu() { ...@@ -812,12 +814,6 @@ function user_menu() {
'context' => MENU_CONTEXT_INLINE, 'context' => MENU_CONTEXT_INLINE,
); );
$items['admin/people/create'] = array(
'title' => 'Add user',
'route_name' => 'user.admin_create',
'type' => MENU_LOCAL_ACTION,
);
// Administration pages. // Administration pages.
$items['admin/config/people'] = array( $items['admin/config/people'] = array(
'title' => 'People', 'title' => 'People',
......
...@@ -40,6 +40,13 @@ user.account_settings: ...@@ -40,6 +40,13 @@ user.account_settings:
requirements: requirements:
_permission: 'administer account settings' _permission: 'administer account settings'
user.admin_account:
path: '/admin/people'
defaults:
_controller: '\Drupal\user\Controller\UserAdmin::userList'
requirements:
_permission: 'administer users'
user.admin_create: user.admin_create:
path: '/admin/people/create' path: '/admin/people/create'
defaults: defaults:
......
...@@ -7,24 +7,104 @@ ...@@ -7,24 +7,104 @@
namespace Drupal\views\EventSubscriber; 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\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents; use Drupal\Core\Routing\RoutingEvents;
use Drupal\views\Plugin\views\display\DisplayRouterInterface; use Drupal\views\Plugin\views\display\DisplayRouterInterface;
use Drupal\views\ViewExecutable;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/** /**
* Builds up the routes of all views. * 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} * {@inheritdoc}
*/ */
public static function getSubscribedEvents() { public static function getSubscribedEvents() {
$events[RoutingEvents::DYNAMIC] = 'dynamicRoutes'; $events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
$events[RoutingEvents::ALTER] = 'alterRoutes';
return $events; 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. * Adds routes defined by all views.
* *
...@@ -34,16 +114,67 @@ public static function getSubscribedEvents() { ...@@ -34,16 +114,67 @@ public static function getSubscribedEvents() {
public function dynamicRoutes(RouteBuildEvent $event) { public function dynamicRoutes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection(); $collection = $event->getRouteCollection();
$views = views_get_applicable_views('uses_route'); foreach ($this->getViewsDisplayIDsWithRoute() as $pair) {
foreach ($views as $data) { list($view_id, $display_id) = explode('.', $pair);
list($view, $display_id) = $data; $view = $this->viewStorageController->load($view_id);
if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) { // @todo This should have an executable factory injected.
if ($display instanceof DisplayRouterInterface) { if (($view = $view->getExecutable()) && $view instanceof ViewExecutable) {
$display->collectRoutes($collection); if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) {
if ($display instanceof DisplayRouterInterface) {
$view_route_names = (array) $display->collectRoutes($collection);
$this->viewRouteNames += $view_route_names;
}
} }
$view->destroy();
} }
$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 @@ ...@@ -7,14 +7,14 @@
namespace Drupal\views\Plugin\views\display; namespace Drupal\views\Plugin\views\display;
use Symfony\Component\Routing\RouteCollection;
/** /**
* Defines an interface for displays that can collect routes. * Defines an interface for displays that can collect routes.
* *
* In addition to implementing the interface, specify 'uses_routes' in the * In addition to implementing the interface, specify 'uses_routes' in the
* plugin definition. * plugin definition.
*/ */
use Symfony\Component\Routing\RouteCollection;
interface DisplayRouterInterface { interface DisplayRouterInterface {
/** /**
...@@ -25,4 +25,17 @@ interface DisplayRouterInterface { ...@@ -25,4 +25,17 @@ interface DisplayRouterInterface {
*/ */
public function collectRoutes(RouteCollection $collection); 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 @@ ...@@ -7,8 +7,12 @@
namespace Drupal\views\Plugin\views\display; 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 Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
...@@ -16,9 +20,59 @@ ...@@ -16,9 +20,59 @@
/**