Commit c9a273af authored by webchick's avatar webchick

Issue #1845402 by Crell, amateescu, dawehner, klausi, g.oechsler: Change...

Issue #1845402 by Crell, amateescu, dawehner, klausi, g.oechsler: Change notice: Update menu link access checks for new router.
parent dad3ef55
......@@ -5,6 +5,8 @@
* API for the Drupal menu system.
*/
use Symfony\Component\HttpFoundation\Request;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Template\Attribute;
......@@ -896,7 +898,11 @@ function _menu_link_translate(&$item, $translate = FALSE) {
}
// menu_tree_check_access() may set this ahead of time for links to nodes.
if (!isset($item['access'])) {
if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) {
if ($route = $item->getRoute()) {
$request = Request::create('/' . $item['path']);
$item['access'] = drupal_container()->get('access_manager')->check($route, $request);
}
elseif (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) {
// An error occurred loading an object.
$item['access'] = FALSE;
return FALSE;
......@@ -2642,6 +2648,17 @@ function menu_router_build() {
$router_items = call_user_func($module . '_menu');
if (isset($router_items) && is_array($router_items)) {
foreach (array_keys($router_items) as $path) {
// If the menu item references a route, normalize the route information
// into the old structure. Note that routes are keyed by name, not path,
// so the path of the route takes precedence.
if (isset($router_items[$path]['route_name'])) {
$router_item = $router_items[$path];
$router_item = _menu_router_merge_route($router_item, $path);
$new_path = $router_item['path'];
unset($router_items[$path]);
$router_items[$new_path] = $router_item;
$path = $new_path;
}
$router_items[$path]['module'] = $module;
}
$callbacks = array_merge($callbacks, $router_items);
......@@ -2655,6 +2672,33 @@ function menu_router_build() {
return array($menu, $masks);
}
/**
* Merges a legacy menu router item with its referenced route.
*
* @param array $router_item
* The router item to modify.
* @param string $path
* The path this router item is for.
*
* @return array
* The modified router item, including the path pattern from the route in the
* 'path' key.
*/
function _menu_router_merge_route(array $router_item, $path) {
$router_item['path'] = $path;
$route_provider = drupal_container()->get('router.route_provider');
$route = $route_provider->getRouteByName($router_item['route_name']);
$router_item['path'] = trim($route->getPattern(), '/');
$router_item['page callback'] = 'USES_ROUTE';
$router_item['access callback'] = TRUE;
return $router_item;
}
/**
* Stores the menu router if we have it in memory.
*/
......
......@@ -9,6 +9,8 @@
* "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
*/
use Symfony\Component\HttpFoundation\Request;
/**
* Check if the current page is the front page.
*
......@@ -209,11 +211,44 @@ function drupal_valid_path($path, $dynamic_allowed = FALSE) {
$item['options'] = '';
_menu_link_translate($item);
}
if (empty($item['access'])) {
// Nothing was found in the old routing system, so try the new one.
$item = _drupal_valid_path_new_router($path);
}
}
else {
$item = menu_get_item($path);
if (empty($item['access'])) {
// Nothing was found in the old routing system, so try the new one.
$item = _drupal_valid_path_new_router($path);
}
}
$menu_admin = FALSE;
return $item && $item['access'];
}
/**
* Temporary helper function to check a path in the new routing system.
*
* @param string $path
* The path string as expected by drupal_valid_path().
*
* @return array|NULL
* An array containing 'access' => TRUE or NULL for paths that were not found
* or the user has no access to.
*/
function _drupal_valid_path_new_router($path) {
$request = Request::create('/' . $path);
$request->attributes->set('system_path', $path);
try {
$dc = drupal_container();
$route = $dc->get('router.dynamic')->matchRequest($request);
if (!empty($route)) {
$dc->get('access_manager')->check($route['_route_object'], $request);
}
return array('access' => TRUE);
}
catch (Exception $e) {
drupal_set_message($e->getMessage(), 'menu', WATCHDOG_ERROR);
}
}
......@@ -31,23 +31,6 @@ class AccessManager extends ContainerAware {
*/
protected $checks;
/**
* The request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* Constructs a new AccessManager.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*/
public function __construct(Request $request) {
$this->request = $request;
}
/**
* Registers a new AccessCheck by service ID.
*
......@@ -106,11 +89,13 @@ protected function applies(Route $route) {
*
* @param \Symfony\Component\Routing\Route $route
* The route to check access to.
* @param \Symfony\Commponent\HttpFoundation\Request $request
* The incoming request object.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If any access check denies access or none explicitly approve.
*/
public function check(Route $route) {
public function check(Route $route, Request $request) {
$access = FALSE;
$checks = $route->getOption('_access_checks') ?: array();
......@@ -120,7 +105,7 @@ public function check(Route $route) {
$this->loadCheck($service_id);
}
$service_access = $this->checks[$service_id]->access($route, $this->request);
$service_access = $this->checks[$service_id]->access($route, $request);
if ($service_access === FALSE) {
// A check has denied access, no need to continue checking.
$access = FALSE;
......@@ -132,10 +117,7 @@ public function check(Route $route) {
}
}
// Access has been denied or not explicily approved.
if (!$access) {
throw new AccessDeniedHttpException();
}
return $access;
}
/**
......
......@@ -256,7 +256,6 @@ public function build(ContainerBuilder $container) {
$container->register('legacy_access_subscriber', 'Drupal\Core\EventSubscriber\LegacyAccessSubscriber')
->addTag('event_subscriber');
$container->register('access_manager', 'Drupal\Core\Access\AccessManager')
->addArgument(new Reference('request'))
->addMethodCall('setContainer', array(new Reference('service_container')));
$container->register('access_subscriber', 'Drupal\Core\EventSubscriber\AccessSubscriber')
->addArgument(new Reference('access_manager'))
......
......@@ -11,6 +11,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Routing\RouteBuildEvent;
......@@ -45,7 +46,10 @@ public function onKernelRequestAccessCheck(GetResponseEvent $event) {
return;
}
$this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
$access = $this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT), $request);
if (!$access) {
throw new AccessDeniedHttpException();
}
}
/**
......
......@@ -158,7 +158,7 @@ protected function convertDrupalItem($router_item) {
// A few menu items have a fake page callback temporarily. Skip those,
// we aren't going to route them.
if ($router_item['page_callback'] == 'NOT_USED') {
if ($router_item['page_callback'] == 'USES_ROUTE') {
throw new ResourceNotFoundException();
}
......
......@@ -166,14 +166,20 @@ public function getRouteByName($name, $parameters = array()) {
*/
public function getRoutesByNames($names, $parameters = array()) {
if (empty($names)) {
throw new \InvalidArgumentException('You must specify the route names to load');
}
$routes_to_load = array_diff($names, array_keys($this->routes));
$result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN (:names)', array(':names' => $routes_to_load));
$routes = $result->fetchAllKeyed();
if ($routes_to_load) {
$result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN (:names)', array(':names' => $routes_to_load));
$routes = $result->fetchAllKeyed();
$return = array();
foreach ($routes as $name => $route) {
$this->routes[$name] = unserialize($route);
$return = array();
foreach ($routes as $name => $route) {
$this->routes[$name] = unserialize($route);
}
}
return array_intersect_key($this->routes, array_flip($names));
......
......@@ -1025,7 +1025,7 @@ function template_preprocess_book_navigation(&$variables) {
}
if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
$parent_href = url($parent['href']);
$parent_href = url($parent['link_path']);
drupal_add_html_head_link(array('rel' => 'up', 'href' => $parent_href));
$variables['parent_url'] = $parent_href;
$variables['parent_title'] = check_plain($parent['title']);
......@@ -1279,14 +1279,7 @@ function book_node_type_update($type) {
* {book} table. FALSE if there is an error.
*/
function book_link_load($mlid) {
if ($item = db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = :mlid", array(
':mlid' => $mlid,
))->fetchAssoc()) {
_menu_link_translate($item);
return $item;
}
return FALSE;
return entity_load('menu_link', $mlid);
}
/**
......
......@@ -194,6 +194,7 @@ function menu_theme() {
* Add a link for each custom menu.
*/
function menu_enable() {
drupal_container()->get('router.builder')->rebuild();
menu_router_rebuild();
$system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system'));
$system_link = reset($system_link);
......
......@@ -62,11 +62,27 @@ protected function buildQuery($ids, $revision_id = FALSE) {
* control the entity load hooks.
*/
protected function attachLoad(&$menu_links, $load_revision = FALSE) {
$routes = array();
foreach ($menu_links as &$menu_link) {
$menu_link->options = unserialize($menu_link->options);
// Use the weight property from the menu link.
$menu_link->router_item['weight'] = $menu_link->weight;
// For all links that have an associated route, load the route object now
// and save it on the object. That way we avoid a select N+1 problem later.
if ($menu_link->route_name) {
$routes[$menu_link->id()] = $menu_link->route_name;
}
}
// Now mass-load any routes needed and associate them.
if ($routes) {
$route_objects = drupal_container()->get('router.route_provider')->getRoutesByNames($routes);
foreach ($routes as $entity_id => $route) {
$menu_links[$entity_id]->setRouteObject($route_objects[$route]);
}
}
parent::attachLoad($menu_links, $load_revision);
......
......@@ -7,6 +7,8 @@
namespace Drupal\menu_link\Plugin\Core\Entity;
use Symfony\Component\Routing\Route;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\ContentEntityInterface;
......@@ -232,6 +234,20 @@ class MenuLink extends Entity implements \ArrayAccess, ContentEntityInterface {
*/
public $updated = 0;
/**
* The name of the route associated with this menu link, if any.
*
* @var string
*/
public $route_name;
/**
* The route object associated with this menu link, if any.
*
* @var \Symfony\Component\Routing\Route
*/
protected $routeObject;
/**
* Overrides Entity::id().
*/
......@@ -248,6 +264,36 @@ public function createDuplicate() {
return $duplicate;
}
/**
* Returns the Route object associated with this link, if any.
*
* @return \Symfony\Component\Routing\Route|null
* The route object for this menu link, or NULL if there isn't one.
*/
public function getRoute() {
if (!$this->route_name) {
return NULL;
}
if (!($this->routeObject instanceof Route)) {
$route_provider = drupal_container()->get('router.route_provider');
$this->routeObject = $route_provider->getRouteByName($this->route_name);
}
return $this->routeObject;
}
/**
* Sets the route object for this link.
*
* This should only be called by MenuLinkStorageController when loading
* the link object. Calling it at other times could result in unpredictable
* behavior.
*
* @param \Symfony\Component\Routing\Route $route
*/
public function setRouteObject(Route $route) {
$this->routeObject = $route;
}
/**
* Resets a system-defined menu link.
*
......
......@@ -197,6 +197,11 @@ function menu_link_schema() {
'default' => 0,
'size' => 'small',
),
'route_name' => array(
'description' => 'The machine name of a defined Symfony Route this menu item represents.',
'type' => 'varchar',
'length' => 255,
),
),
'indexes' => array(
'path_menu' => array(array('link_path', 128), 'menu_name'),
......
......@@ -33,7 +33,6 @@ public static function getInfo() {
* Tests permission requirements on routes.
*/
public function testPermissionAccess() {
$this->drupalGet('router_test/test7');
$this->assertResponse(403, "Access denied for a route where we don't have a permission");
......
......@@ -2181,6 +2181,19 @@ function system_update_8050() {
));
}
/**
* Adds route_name column to the menu_links table.
*/
function system_update_8051() {
$spec = array(
'description' => 'The machine name of a defined Symfony Route this menu item represents.',
'type' => 'varchar',
'length' => 255,
);
db_add_field('menu_links', 'route_name', $spec);
}
/**
* @} End of "defgroup updates-7.x-to-8.x".
* The next series of updates should start at 9000.
......
......@@ -875,29 +875,6 @@ function user_register_access() {
* Implements hook_menu().
*/
function user_menu() {
// @todo Remove once drupal_valid_path() is fixed to find and access check
// paths managed by the new routing system: http://drupal.org/node/1793520.
$items['user/autocomplete'] = array(
'title' => 'User autocomplete',
// _menu_router_build() denies access to paths without a page callback.
'page callback' => 'NOT_USED',
'access callback' => 'user_access',
'access arguments' => array('access user profiles'),
'type' => MENU_CALLBACK,
'file' => 'user.pages.inc',
);
$items['user/autocomplete/anonymous'] = array(
'title' => 'User autocomplete including anonymous',
// _menu_router_build() denies access to paths without a page callback.
'page callback' => 'NOT_USED',
'page arguments' => array(TRUE),
'access callback' => 'user_access',
'access arguments' => array('access user profiles'),
'type' => MENU_CALLBACK,
'file' => 'user.pages.inc',
);
// Registration and login pages.
$items['user'] = array(
'title' => 'User account',
......@@ -923,13 +900,7 @@ function user_menu() {
$items['user/register'] = array(
'title' => 'Create new account',
'type' => MENU_LOCAL_TASK,
// @todo This route is now declared in user.routing.yml, so the below are
// only needed for drupal_valid_path(). Without a non-empty (but not
// necessarily valid) page callback, _menu_router_build() overrides the
// access callback to 0. Remove once drupal_valid_path works with the new
// routing system: http://drupal.org/node/1793520.
'page callback' => 'NOT_USED',
'access callback' => 'user_register_access',
'route_name' => 'user_register',
);
$items['user/password'] = array(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment