diff --git a/core/includes/menu.inc b/core/includes/menu.inc index ddbaca2..80be82d 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -5,19 +5,12 @@ * API for the Drupal menu system. */ -use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\String; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; -use Drupal\Core\ParamConverter\ParamNotConvertedException; -use Drupal\Core\Routing\RequestHelper; use Drupal\Core\Template\Attribute; -use Drupal\menu_link\Entity\MenuLink; use Drupal\menu_link\MenuLinkInterface; -use Drupal\menu_link\MenuLinkStorageController; use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Route; /** * @defgroup menu Menu and routing system @@ -312,55 +305,6 @@ const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'; /** - * Unserializes menu data, using a map to replace path elements. - * - * The menu system stores various path-related information (such as the 'page - * arguments' and 'access arguments' components of a menu item) in the database - * using serialized arrays, where integer values in the arrays represent - * arguments to be replaced by values from the path. This function first - * unserializes such menu information arrays, and then does the path - * replacement. - * - * The path replacement acts on each integer-valued element of the unserialized - * menu data array ($data) using a map array ($map, which is typically an array - * of path arguments) as a list of replacements. For instance, if there is an - * element of $data whose value is the number 2, then it is replaced in $data - * with $map[2]; non-integer values in $data are left alone. - * - * As an example, an unserialized $data array with elements ('node_load', 1) - * represents instructions for calling the node_load() function. Specifically, - * this instruction says to use the path component at index 1 as the input - * parameter to node_load(). If the path is 'node/123', then $map will be the - * array ('node', 123), and the returned array from this function will have - * elements ('node_load', 123), since $map[1] is 123. This return value will - * indicate specifically that node_load(123) is to be called to load the node - * whose ID is 123 for this menu item. - * - * @param $data - * A serialized array of menu data, as read from the database. - * @param $map - * A path argument array, used to replace integer values in $data; an integer - * value N in $data will be replaced by value $map[N]. Typically, the $map - * array is generated from a call to the arg() function. - * - * @return - * The unserialized $data array, with path arguments replaced. - */ -function menu_unserialize($data, $map) { - if ($data = unserialize($data)) { - foreach ($data as $k => $v) { - if (is_int($v)) { - $data[$k] = isset($map[$v]) ? $map[$v] : ''; - } - } - return $data; - } - else { - return array(); - } -} - -/** * Localizes the router item title using t() or another callback. * * Translate the title and description to allow storage of English title @@ -369,29 +313,12 @@ function menu_unserialize($data, $map) { * * @param $item * A menu router item or a menu link item. - * @param $map - * The path as an array with objects already replaced. E.g., for path - * node/123 $map would be array('node', $node) where $node is the node - * object for node 123. * @param $link_translate * TRUE if we are translating a menu link item; FALSE if we are * translating a menu router item. - * - * @return - * No return value. - * $item['title'] is localized according to $item['title_callback']. - * If an item's callback is check_plain(), $item['options']['html'] becomes - * TRUE. - * $item['description'] is computed using $item['description_callback'] if - * specified; otherwise it is translated using t(). - * When doing link translation and the $item['options']['attributes']['title'] - * (link title attribute) matches the description, it is translated as well. */ -function _menu_item_localize(&$item, $map, $link_translate = FALSE) { +function _menu_item_localize(&$item, $link_translate = FALSE) { // Allow default menu links to be translated. - // @todo Figure out a proper way to support translations of menu links, see - // https://drupal.org/node/2193777. - $title_callback = $item instanceof MenuLinkInterface && !$item->customized ? 't' : $item['title_callback']; $item['localized_options'] = $item['options']; // All 'class' attributes are assumed to be an array during rendering, but // links stored in the database may use an old string value. @@ -412,104 +339,18 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) { // we handle it directly instead of using indirect, slower methods. // @todo Recheck this line once https://drupal.org/node/2084421 is in. $item['title'] = isset($item['link_title']) ? $item['link_title'] : $item['title']; - if ($title_callback == 't') { - if (empty($item['title_arguments'])) { - $item['title'] = t($item['title']); - } - else { - $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map)); - } - } - elseif ($title_callback) { - if (empty($item['title_arguments'])) { - $item['title'] = $title_callback($item['title']); - } - else { - $item['title'] = call_user_func_array($title_callback, menu_unserialize($item['title_arguments'], $map)); - } - // Avoid calling check_plain again on l() function. - if ($title_callback == 'check_plain') { - $item['localized_options']['html'] = TRUE; - } - } + // @todo Figure out a proper way to support translations of menu links, see + // https://drupal.org/node/2193777. + $item['title'] = t($item['title']); } elseif ($link_translate) { $item['title'] = $item['link_title']; } - - // Translate description, see the motivation above. - if (!empty($item['description'])) { - $original_description = $item['description']; - } - if (!empty($item['description_arguments']) || !empty($item['description'])) { - $description_callback = $item['description_callback']; - // If the description callback is t(), call it directly. - if ($description_callback == 't') { - if (empty($item['description_arguments'])) { - $item['description'] = t($item['description']); - } - else { - $item['description'] = t($item['description'], menu_unserialize($item['description_arguments'], $map)); - } - } - elseif ($description_callback) { - // If there are no arguments, call the description callback directly. - if (empty($item['description_arguments'])) { - $item['description'] = $description_callback($item['description']); - } - // Otherwise, use call_user_func_array() to pass the arguments. - else { - $item['description'] = call_user_func_array($description_callback, menu_unserialize($item['description_arguments'], $map)); - } - } - } - // If the title and description are the same, use the translated description - // as a localized title. - if ($link_translate && isset($original_description) && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) { - $item['localized_options']['attributes']['title'] = $item['description']; - } -} - -/** - * Handles dynamic path translation and menu access control. - * - * When a user arrives on a page such as node/5, this function determines - * what "5" corresponds to, by inspecting the page's menu path definition, - * node/%node. This will call node_load(5) to load the corresponding node - * object. - * - * It also works in reverse, to allow the display of tabs and menu items which - * contain these dynamic arguments, translating node/%node to node/5. - * - * Translation of menu item titles and descriptions are done here to - * allow for storage of English strings in the database, and translation - * to the language required to generate the current page. - * - * @param $router_item - * A menu router item - * @param string - * The path. - */ -function _menu_translate(&$router_item, $path) { - // Generate the link path for the page request or local tasks. - $route = \Drupal::service('router.route_provider')->getRouteByName($router_item['route_name']); - $request = RequestHelper::duplicate(\Drupal::request(), '/' . $path); - $request->attributes->set('_system_path', $path); - $router_item['access'] = menu_item_route_access($route, $request); - - // For performance, don't localize an item the user can't access. - if ($router_item['access']) { - $map = explode('/', $path); - _menu_item_localize($router_item, $map); - } } /** * Provides menu link unserializing, access control, and argument handling. * - * This function is similar to _menu_translate(), but it also does - * link-specific preparation. - * * @param array $item * The passed in item has the following keys: * - access: (optional) Becomes TRUE if the item is accessible, FALSE @@ -549,7 +390,7 @@ function _menu_link_translate(&$item) { } // For performance, don't localize a link the user can't access. if ($item['access']) { - _menu_item_localize($item, array(), TRUE); + _menu_item_localize($item, TRUE); } } @@ -562,32 +403,6 @@ function _menu_link_translate(&$item) { } /** - * Checks access to a menu item by mocking a request for a path. - * - * @param \Symfony\Component\Routing\Route $route - * Router for the given menu item. - * @param \Symfony\Component\HttpFoundation\Request $request - * The current request object, used to find the current route. - * - * @return bool - * TRUE if the user has access or FALSE if the user should be presented - * with access denied. - * - */ -function menu_item_route_access(Route $route, Request $request) { - // Attempt to match this path to provide a fully built request to the - // access checker. - try { - $request->attributes->add(\Drupal::service('router')->matchRequest($request)); - } - catch (ParamNotConvertedException $e) { - return FALSE; - } - - return \Drupal::service('access_manager')->check($route, $request, \Drupal::currentUser()); -} - -/** * Renders a menu tree based on the current path. * * @param $menu_name @@ -1757,8 +1572,9 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { foreach ($menu_names as $menu_name) { if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) { $candidate_item = $candidates[$link_path][$menu_name]; - _menu_translate($candidate_item, $path); + $candidate_item['access'] = \Drupal::service('access_manager')->checkNamedRoute($candidate_item['route_name'], $candidate_item['route_parameters'], \Drupal::currentUser()); if ($candidate_item['access']) { + _menu_item_localize($candidate_item); $preferred_links[$path][$menu_name] = $candidate_item; if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) { // Store the most specific link. diff --git a/core/includes/path.inc b/core/includes/path.inc index ef169f4..3193d0e 100644 --- a/core/includes/path.inc +++ b/core/includes/path.inc @@ -5,8 +5,9 @@ * Functions to handle paths in Drupal. */ +use Drupal\Component\Utility\Url; +use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\Core\Routing\RequestHelper; -use Symfony\Component\HttpFoundation\Request; /** * Check if the current page is the front page. @@ -191,22 +192,34 @@ function path_get_admin_paths() { * FALSE otherwise. */ function drupal_valid_path($path) { - $is_valid = FALSE; // External URLs and the front page are always valid. - if ($path == '' || url_is_external($path)) { - $is_valid = TRUE; + if ($path == '' || Url::isExternal($path)) { + return TRUE; } // Check the routing system. - elseif (($collection = \Drupal::service('router.route_provider')->getRoutesByPattern('/' . $path)) && $collection->count() > 0) { - $routes = $collection->all(); - $route = reset($routes); - $request = RequestHelper::duplicate(\Drupal::request(), '/' . $path); - $request->attributes->set('_system_path', $path); - - // We indicate that a menu administrator is running the menu access check. - $request->attributes->set('_menu_admin', TRUE); - $is_valid = menu_item_route_access($route, $request); + $collection = \Drupal::service('router.route_provider')->getRoutesByPattern('/' . $path); + if ($collection->count() == 0) { + return FALSE; } - return $is_valid; + + $request = RequestHelper::duplicate(\Drupal::request(), '/' . $path); + $request->attributes->set('_system_path', $path); + + // We indicate that a menu administrator is running the menu access check. + $request->attributes->set('_menu_admin', TRUE); + + // Attempt to match this path to provide a fully built request to the + // access checker. + try { + $request->attributes->add(\Drupal::service('router')->matchRequest($request)); + } + catch (ParamNotConvertedException $e) { + return FALSE; + } + + // Consult the accsss manager. + $routes = $collection->all(); + $route = reset($routes); + return \Drupal::service('access_manager')->check($route, $request, \Drupal::currentUser()); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php index 476d006..72fe06a 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php @@ -112,6 +112,15 @@ protected function doTestTitleMenuCallback() { } /** + * Tests menu item descriptions. + */ + protected function doTestDescriptionMenuItems() { + // Verify that the menu router item title is output as page title. + $this->drupalGet('menu_callback_description'); + $this->assertText(t('Menu item description text')); + } + + /** * Tests for menu_link_maintain(). */ protected function doTestMenuLinkMaintain() { diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterPermissionTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterPermissionTest.php index d70bb18..e33156d 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterPermissionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterPermissionTest.php @@ -38,19 +38,6 @@ public function testPermissionAccess() { $path = 'router_test/test7'; $this->drupalGet($path); $this->assertResponse(403, "Access denied for a route where we don't have a permission"); - // An invalid path should throw an exception. - $path .= 'invalid'; - $request = RequestHelper::duplicate(\Drupal::request(), '/' . $path); - $request->attributes->set('_system_path', $path); - $route = \Drupal::service('router.route_provider')->getRouteByName('router_test.7'); - try { - menu_item_route_access($route, $request); - $exception = FALSE; - } - catch (ResourceNotFoundException $e) { - $exception = TRUE; - } - $this->assertTrue($exception, 'A ResourceNotFoundException was thrown while checking access for an invalid route.'); $this->drupalGet('router_test/test8'); $this->assertResponse(403, 'Access denied by default if no access specified'); diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module index d718848..08e6382 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -17,6 +17,20 @@ function menu_test_menu_link_defaults() { 'route_name' => 'menu_test.menu_name_test', 'menu_name' => menu_test_menu_name(), ); + // This item uses SystemController::systemAdminMenuBlockPage() to list child + // items. + $items['menu_test.menu_callback_description'] = array( + 'link_title' => 'Menu item title', + 'description' => 'Menu item description parent', + 'route_name' => 'menu_test.callback_description', + ); + // This item tests the description key. + $items['menu_test.menu_callback_description.description-plain'] = array( + 'link_title' => 'Menu item with a regular description', + 'description' => 'Menu item description text', + 'route_name' => 'menu_test.callback_description_plain', + 'parent' => 'menu_test.menu_callback_description', + ); $items['menu_test.menu_no_title_callback'] = array( 'link_title' => 'A title with @placeholder', diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml index 9680e0f..34d5dad 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml @@ -21,6 +21,21 @@ menu_test.login_callback: requirements: _access: 'TRUE' +menu_test.callback_description: + path: '/menu_callback_description' + defaults: + _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + requirements: + _access: 'TRUE' + +menu_test.callback_description_plain: + path: '/menu_callback_description/description-plain' + defaults: + _title: 'Menu item with a regular description' + _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback' + requirements: + _access: 'TRUE' + menu_test.menu_no_title_callback: path: '/menu_no_title_callback' defaults: