diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 251b81d..b883a1e 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -5,15 +5,2522 @@ * API for the Drupal menu system. */ +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Routing\RequestHelper; +use Drupal\Core\Template\Attribute; +use Drupal\menu_link\Entity\MenuLink; +use Drupal\menu_link\MenuLinkStorageController; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Route; + /** - * @addtogroup menu + * @defgroup menu Menu system + * @{ + * Define the navigation menus, and route page requests to code based on URLs. + * + * The Drupal menu system drives both the navigation system from a user + * perspective and the callback system that Drupal uses to respond to URLs + * passed from the browser. For this reason, a good understanding of the + * menu system is fundamental to the creation of complex modules. As a note, + * this is related to, but separate from menu.module, which allows menus + * (which in this context are hierarchical lists of links) to be customized from + * the Drupal administrative interface. + * + * Drupal's menu system follows a simple hierarchy defined by paths. + * Implementations of hook_menu() define menu items and assign them to + * paths (which should be unique). The menu system aggregates these items + * and determines the menu hierarchy from the paths. For example, if the + * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system + * would form the structure: + * - a + * - a/b + * - a/b/c/d + * - a/b/h + * - e + * - f/g + * Note that the number of elements in the path does not necessarily + * determine the depth of the menu item in the tree. + * + * When responding to a page request, the menu system looks to see if the + * path requested by the browser is registered as a menu item with a + * callback. If not, the system searches up the menu tree for the most + * complete match with a callback it can find. If the path a/b/i is + * requested in the tree above, the callback for a/b would be used. + * + * The found callback function is called with any arguments specified + * in the "page arguments" attribute of its menu item. The + * attribute must be an array. After these arguments, any remaining + * components of the path are appended as further arguments. In this + * way, the callback for a/b above could respond to a request for + * a/b/i differently than a request for a/b/j. + * + * For an illustration of this process, see page_example.module. + * + * Access to the callback functions is also protected by the menu system. + * The "access callback" with an optional "access arguments" of each menu + * item is called before the page callback proceeds. If this returns TRUE, + * then access is granted; if FALSE, then access is denied. Default local task + * menu items (see next paragraph) may omit this attribute to use the value + * provided by the parent item. + * + * In the default Drupal interface, you will notice many links rendered as + * tabs. These are known in the menu system as "local tasks", and they are + * rendered as tabs by default, though other presentations are possible. + * Local tasks function just as other menu items in most respects. It is + * convention that the names of these tasks should be short verbs if + * possible. In addition, a "default" local task should be provided for + * each set. When visiting a local task's parent menu item, the default + * local task will be rendered as if it is selected; this provides for a + * normal tab user experience. This default task is special in that it + * links not to its provided path, but to its parent item's path instead. + * The default task's path is only used to place it appropriately in the + * menu hierarchy. + * + * Everything described so far is stored in the menu_router table. The + * menu_links table holds the visible menu links. By default these are + * derived from the same hook_menu definitions, however you are free to + * add more with menu_link_save(). + */ + +/** + * @defgroup menu_flags Menu flags + * @{ + * Flags for use in the "type" attribute of menu items. + */ + +/** + * Internal menu flag -- menu item is the root of the menu tree. + */ +const MENU_IS_ROOT = 0x0001; + +/** + * Internal menu flag -- menu item is visible in the menu tree. + */ +const MENU_VISIBLE_IN_TREE = 0x0002; + +/** + * Internal menu flag -- menu item is visible in the breadcrumb. + */ +const MENU_VISIBLE_IN_BREADCRUMB = 0x0004; + +/** + * Internal menu flag -- menu item links back to its parent. + */ +const MENU_LINKS_TO_PARENT = 0x0008; + +/** + * Internal menu flag -- menu item can be modified by administrator. + */ +const MENU_MODIFIED_BY_ADMIN = 0x0020; + +/** + * Internal menu flag -- menu item was created by administrator. + */ +const MENU_CREATED_BY_ADMIN = 0x0040; + +/** + * Internal menu flag -- menu item is a local task. + */ +const MENU_IS_LOCAL_TASK = 0x0080; + +/** + * Internal menu flag -- menu item is a local action. + */ +const MENU_IS_LOCAL_ACTION = 0x0100; + +/** + * @} End of "defgroup menu_flags". + */ + +/** + * @defgroup menu_item_types Menu item types + * @{ + * Definitions for various menu item types. + * + * Menu item definitions provide one of these constants, which are shortcuts for + * combinations of @link menu_flags Menu flags @endlink. + */ + +/** + * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs. + * + * Normal menu items show up in the menu tree and can be moved/hidden by + * the administrator. Use this for most menu items. It is the default value if + * no menu item type is specified. + */ +define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB); + +/** + * Menu type -- A hidden, internal callback, typically used for API calls. + * + * Callbacks simply register a path so that the correct function is fired + * when the URL is accessed. They do not appear in menus or breadcrumbs. + */ +const MENU_CALLBACK = 0x0000; + +/** + * Menu type -- A normal menu item, hidden until enabled by an administrator. + * + * Modules may "suggest" menu items that the administrator may enable. They act + * just as callbacks do until enabled, at which time they act like normal items. + * Note for the value: 0x0010 was a flag which is no longer used, but this way + * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate. + */ +define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010); + +/** + * Menu type -- A task specific to the parent item, usually rendered as a tab. + * + * Local tasks are menu items that describe actions to be performed on their + * parent item. An example is the path "node/52/edit", which performs the + * "edit" task on "node/52". + */ +define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB); + +/** + * Menu type -- The "default" local task, which is initially active. + * + * Every set of local tasks should provide one "default" task, that links to the + * same path as its parent when clicked. + */ +define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT | MENU_VISIBLE_IN_BREADCRUMB); + +/** + * Menu type -- An action specific to the parent, usually rendered as a link. + * + * Local actions are menu items that describe actions on the parent item such + * as adding a new user, taxonomy term, etc. + */ +define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB); + +/** + * Menu type -- A task specific to the parent, which is never rendered. + * + * Sibling local tasks are not rendered themselves, but affect the breadcrumb + * trail and need their sibling tasks rendered as tabs. + */ +define('MENU_SIBLING_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB); + +/** + * @} End of "defgroup menu_item_types". + */ + +/** + * @defgroup menu_context_types Menu context types + * @{ + * Flags for use in the "context" attribute of menu router items. + */ + +/** + * Internal menu flag: Invisible local task. + * + * This flag may be used for local tasks like "Delete", so custom modules and + * themes can alter the default context and expose the task by altering menu. + */ +const MENU_CONTEXT_NONE = 0x0000; + +/** + * Internal menu flag: Local task should be displayed in page context. + */ +const MENU_CONTEXT_PAGE = 0x0001; + +/** + * Internal menu flag: Local task should be displayed inline. + */ +const MENU_CONTEXT_INLINE = 0x0002; + +/** + * @} End of "defgroup menu_context_types". + */ + +/** + * @defgroup menu_status_codes Menu status codes + * @{ + * Status codes for menu callbacks. + */ + +/** + * Internal menu status code -- Menu item was not found. + */ +const MENU_NOT_FOUND = 404; + +/** + * Internal menu status code -- Menu item access is denied. + */ +const MENU_ACCESS_DENIED = 403; + +/** + * Internal menu status code -- Menu item inaccessible because site is offline. + */ +const MENU_SITE_OFFLINE = 4; + +/** + * Internal menu status code -- Everything is working fine. + */ +const MENU_SITE_ONLINE = 5; + +/** + * @} End of "defgroup menu_status_codes". + */ + +/** + * @defgroup menu_tree_parameters Menu tree parameters * @{ + * Parameters for a menu tree. + */ + + /** + * The maximum number of path elements for a menu callback + */ +const MENU_MAX_PARTS = 9; + + +/** + * The maximum depth of a menu links tree - matches the number of p columns. + * + * @todo Move this constant to MenuLinkStorageController along with all the tree + * functionality. + */ +const MENU_MAX_DEPTH = 9; + + +/** + * @} End of "defgroup menu_tree_parameters". */ -use Drupal\Component\Utility\SafeMarkup; -use Drupal\Component\Utility\String; -use Drupal\Core\Render\Element; +/** + * Reserved key to identify the most specific menu link for a given path. + * + * The value of this constant is a hash of the constant name. We use the hash + * so that the reserved key is over 32 characters in length and will not + * collide with allowed menu names: + * @code + * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91 + * @endcode + * + * @see menu_link_get_preferred() + */ +const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'; + +/** + * Returns the ancestors (and relevant placeholders) for any given path. + * + * For example, the ancestors of node/12345/edit are: + * - node/12345/edit + * - node/12345/% + * - node/%/edit + * - node/%/% + * - node/12345 + * - node/% + * - node + * + * To generate these, we will use binary numbers. Each bit represents a + * part of the path. If the bit is 1, then it represents the original + * value while 0 means wildcard. If the path is node/12/edit/foo + * then the 1011 bitstring represents node/%/edit/foo where % means that + * any argument matches that part. We limit ourselves to using binary + * numbers that correspond the patterns of wildcards of router items that + * actually exists. This list of 'masks' is built in menu_router_rebuild(). + * + * @param $parts + * An array of path parts, for the above example + * array('node', '12345', 'edit'). + * + * @return + * An array which contains the ancestors and placeholders. Placeholders + * simply contain as many '%s' as the ancestors. + */ +function menu_get_ancestors($parts) { + $number_parts = count($parts); + $ancestors = array(); + $length = $number_parts - 1; + $end = (1 << $number_parts) - 1; + $masks = Drupal::state()->get('menu.masks'); + // If the optimized menu.masks array is not available use brute force to get + // the correct $ancestors and $placeholders returned. Do not use this as the + // default value of the menu.masks variable to avoid building such a big + // array. + if (!$masks) { + $masks = range(511, 1); + } + // Only examine patterns that actually exist as router items (the masks). + foreach ($masks as $i) { + if ($i > $end) { + // Only look at masks that are not longer than the path of interest. + continue; + } + elseif ($i < (1 << $length)) { + // We have exhausted the masks of a given length, so decrease the length. + --$length; + } + $current = ''; + for ($j = $length; $j >= 0; $j--) { + // Check the bit on the $j offset. + if ($i & (1 << $j)) { + // Bit one means the original value. + $current .= $parts[$length - $j]; + } + else { + // Bit zero means means wildcard. + $current .= '%'; + } + // Unless we are at offset 0, add a slash. + if ($j) { + $current .= '/'; + } + } + $ancestors[] = $current; + } + return $ancestors; +} + +/** + * 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(); + } +} + + + +/** + * Replaces the statically cached item for a given path. + * + * @param $path + * The path. + * @param $router_item + * The router item. Usually a router entry from menu_get_item() is either + * modified or set to a different path. This allows the navigation block, + * the page title, the breadcrumb, and the page help to be modified in one + * call. + */ +function menu_set_item($path, $router_item) { + menu_get_item($path, $router_item); +} + +/** + * Gets a router item. + * + * @param $path + * The path; for example, 'node/5'. The function will find the corresponding + * node/% item and return that. + * @param $router_item + * Internal use only. + * + * @return + * The router item or, if an error occurs in _menu_translate(), FALSE. A + * router item is an associative array corresponding to one row in the + * menu_router table. The value corresponding to the key 'map' holds the + * loaded objects. The value corresponding to the key 'access' is TRUE if the + * current user can access this page. The values corresponding to the keys + * 'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will + * be filled in based on the database values and the objects loaded. + */ +function menu_get_item($path = NULL, $router_item = NULL) { + $router_items = &drupal_static(__FUNCTION__); + if (!isset($path)) { + $path = current_path(); + } + if (isset($router_item)) { + $router_items[$path] = $router_item; + } + if (!isset($router_items[$path])) { + // Rebuild if we know it's needed, or if the menu masks are missing which + // occurs rarely, likely due to a race condition of multiple rebuilds. + if (Drupal::state()->get('menu_rebuild_needed') || !Drupal::state()->get('menu.masks')) { + menu_router_rebuild(); + } + $original_map = arg(NULL, $path); + + $parts = array_slice($original_map, 0, MENU_MAX_PARTS); + $ancestors = menu_get_ancestors($parts); + $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc(); + + if ($router_item) { + // Allow modules to alter the router item before it is translated and + // checked for access. + drupal_alter('menu_get_item', $router_item, $path, $original_map); + + $map = _menu_translate($router_item, $original_map); + $router_item['original_map'] = $original_map; + if ($map === FALSE) { + $router_items[$path] = FALSE; + return FALSE; + } + if ($router_item['access']) { + $router_item['map'] = $map; + $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts'])); + $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts'])); + } + } + $router_items[$path] = $router_item; + } + return $router_items[$path]; +} + +/** + * Loads objects into the map as defined in the $item['load_functions']. + * + * @param $item + * A menu router or menu link item + * @param $map + * An array of path arguments; for example, array('node', '5'). + * + * @return + * Returns TRUE for success, FALSE if an object cannot be loaded. + * Names of object loading functions are placed in $item['load_functions']. + * Loaded objects are placed in $map[]; keys are the same as keys in the + * $item['load_functions'] array. + * $item['access'] is set to FALSE if an object cannot be loaded. + */ +function _menu_load_objects(&$item, &$map) { + if ($load_functions = $item['load_functions']) { + // If someone calls this function twice, then unserialize will fail. + if (!is_array($load_functions)) { + $load_functions = unserialize($load_functions); + } + $path_map = $map; + foreach ($load_functions as $index => $function) { + if ($function) { + $value = isset($path_map[$index]) ? $path_map[$index] : ''; + if (is_array($function)) { + // Set up arguments for the load function. These were pulled from + // 'load arguments' in the hook_menu() entry, but they need + // some processing. In this case the $function is the key to the + // load_function array, and the value is the list of arguments. + list($function, $args) = each($function); + $load_functions[$index] = $function; + + // Some arguments are placeholders for dynamic items to process. + foreach ($args as $i => $arg) { + if ($arg === '%index') { + // Pass on argument index to the load function, so multiple + // occurrences of the same placeholder can be identified. + $args[$i] = $index; + } + if ($arg === '%map') { + // Pass on menu map by reference. The accepting function must + // also declare this as a reference if it wants to modify + // the map. + $args[$i] = &$map; + } + if (is_int($arg)) { + $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : ''; + } + } + array_unshift($args, $value); + $return = call_user_func_array($function, $args); + } + else { + $return = $function($value); + } + // If callback returned an error or there is no callback, trigger 404. + if (empty($return)) { + $item['access'] = FALSE; + $map = FALSE; + return FALSE; + } + $map[$index] = $return; + } + } + $item['load_functions'] = $load_functions; + } + return TRUE; +} + +/** + * Checks access to a menu item using the access callback. + * + * @param $item + * A menu router or menu link item + * @param $map + * An array of path arguments; for example, array('node', '5'). + * + * @return + * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. + */ +function _menu_check_access(&$item, $map) { + // Determine access callback, which will decide whether or not the current + // user has access to this path. + $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']); + // Check for a TRUE or FALSE value. + if (is_numeric($callback)) { + $item['access'] = (bool) $callback; + } + else { + $arguments = menu_unserialize($item['access_arguments'], $map); + // As call_user_func_array is quite slow and user_access is a very common + // callback, it is worth making a special case for it. + if ($callback == 'user_access') { + $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]); + } + else { + $item['access'] = call_user_func_array($callback, $arguments); + } + } +} + +/** + * Localizes the router item title using t() or another callback. + * + * Translate the title and description to allow storage of English title + * strings in the database, yet display of them in the language required + * by the current user. + * + * @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) { + $title_callback = $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. + // @todo In order to remove this code we need to implement a database update + // including unserializing all existing link options and running this code + // on them, as well as adding validation to menu_link_save(). + if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) { + $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']); + } + // If we are translating the title of a menu link, and its title is the same + // as the corresponding router item, then we can use the title information + // from the router. If it's customized, then we need to use the link title + // itself; can't localize. + // If we are translating a router item (tabs, page, breadcrumb), then we + // can always use the information from the router item. + if (!$link_translate || ($item['title'] == $item['link_title'])) { + // t() is a special case. Since it is used very close to all the time, + // we handle it directly instead of using indirect, slower methods. + 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; + } + } + } + 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 $map + * An array of path arguments; for example, array('node', '5'). + * @param $to_arg + * Execute $item['to_arg_functions'] or not. Use only if you want to render a + * path from the menu table, for example tabs. + * + * @return + * Returns the map with objects loaded as defined in the + * $item['load_functions']. $item['access'] becomes TRUE if the item is + * accessible, FALSE otherwise. $item['href'] is set according to the map. + * If an error occurs during calling the load_functions (like trying to load + * a non-existent node) then this function returns FALSE. + */ +function _menu_translate(&$router_item, $map, $to_arg = FALSE) { + if ($to_arg && !empty($router_item['to_arg_functions'])) { + // Fill in missing path elements, such as the current uid. + _menu_link_map_translate($map, $router_item['to_arg_functions']); + } + // The $path_map saves the pieces of the path as strings, while elements in + // $map may be replaced with loaded objects. + $path_map = $map; + if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) { + // An error occurred loading an object. + $router_item['access'] = FALSE; + return FALSE; + } + + // Generate the link path for the page request or local tasks. + $link_map = explode('/', $router_item['path']); + if (isset($router_item['tab_root'])) { + $tab_root_map = explode('/', $router_item['tab_root']); + } + if (isset($router_item['tab_parent'])) { + $tab_parent_map = explode('/', $router_item['tab_parent']); + } + for ($i = 0; $i < $router_item['number_parts']; $i++) { + if ($link_map[$i] == '%') { + $link_map[$i] = $path_map[$i]; + } + if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') { + $tab_root_map[$i] = $path_map[$i]; + } + if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') { + $tab_parent_map[$i] = $path_map[$i]; + } + } + $router_item['href'] = implode('/', $link_map); + $router_item['tab_root_href'] = implode('/', $tab_root_map); + $router_item['tab_parent_href'] = implode('/', $tab_parent_map); + $router_item['options'] = array(); + if (!empty($router_item['route_name'])) { + // Route-provided menu items do not have menu loaders, so replace the map + // with the link map. + $map = $link_map; + + $route_provider = Drupal::getContainer()->get('router.route_provider'); + $route = $route_provider->getRouteByName($router_item['route_name']); + $router_item['access'] = menu_item_route_access($route, $router_item['href'], $map); + } + else { + // @todo: Remove once all routes are converted. + _menu_check_access($router_item, $map); + } + // For performance, don't localize an item the user can't access. + if ($router_item['access']) { + _menu_item_localize($router_item, $map); + } + + return $map; +} + +/** + * Translates the path elements in the map using any to_arg helper function. + * + * @param $map + * An array of path arguments; for example, array('node', '5'). + * @param $to_arg_functions + * An array of helper functions; for example, array(2 => 'menu_tail_to_arg'). + * + * @see hook_menu() + */ +function _menu_link_map_translate(&$map, $to_arg_functions) { + $to_arg_functions = unserialize($to_arg_functions); + foreach ($to_arg_functions as $index => $function) { + // Translate place-holders into real values. + $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index); + if (!empty($map[$index]) || isset($arg)) { + $map[$index] = $arg; + } + else { + unset($map[$index]); + } + } +} + +/** + * Returns a string containing the path relative to the current index. + */ +function menu_tail_to_arg($arg, $map, $index) { + return implode('/', array_slice($map, $index)); +} + +/** + * Loads the path as one string relative to the current index. + * + * To use this load function, you must specify the load arguments + * in the router item as: + * @code + * $item['load arguments'] = array('%map', '%index'); + * @endcode + * + * @see search_menu(). + */ +function menu_tail_load($arg, &$map, $index) { + $arg = implode('/', array_slice($map, $index)); + $map = array_slice($map, 0, $index); + return $arg; +} + +/** + * Provides menu link access control, translation, and argument handling. + * + * This function is similar to _menu_translate(), but it also does + * link-specific preparation (such as always calling to_arg() functions). + * + * @param $item + * A menu link. + * @param $translate + * (optional) Whether to try to translate a link containing dynamic path + * argument placeholders (%) based on the menu router item of the current + * path. Defaults to FALSE. Internally used for breadcrumbs. + * + * @return + * Returns the map of path arguments with objects loaded as defined in the + * $item['load_functions']. + * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. + * $item['href'] is generated from link_path, possibly by to_arg functions. + * $item['title'] is generated from link_title, and may be localized. + * $item['options'] is unserialized; it is also changed within the call here + * to $item['localized_options'] by _menu_item_localize(). + */ +function _menu_link_translate(&$item, $translate = FALSE) { + if (!is_array($item['options'])) { + $item['options'] = unserialize($item['options']); + } + if ($item['external']) { + $item['access'] = 1; + $map = array(); + $item['href'] = $item['link_path']; + $item['title'] = $item['link_title']; + $item['localized_options'] = $item['options']; + } + else { + // Complete the path of the menu link with elements from the current path, + // if it contains dynamic placeholders (%). + $map = explode('/', $item['link_path']); + if (strpos($item['link_path'], '%') !== FALSE) { + // Invoke registered to_arg callbacks. + if (!empty($item['to_arg_functions'])) { + _menu_link_map_translate($map, $item['to_arg_functions']); + } + // Or try to derive the path argument map from the current router item, + // if this $item's path is within the router item's path. This means + // that if we are on the current path 'foo/%/bar/%/baz', then + // menu_get_item() will have translated the menu router item for the + // current path, and we can take over the argument map for a link like + // 'foo/%/bar'. This inheritance is only valid for breadcrumb links. + // @see _menu_tree_check_access() + // @see menu_get_active_breadcrumb() + elseif ($translate && ($current_router_item = menu_get_item())) { + // If $translate is TRUE, then this link is in the active trail. + // Only translate paths within the current path. + if (strpos($current_router_item['path'], $item['link_path']) === 0) { + $count = count($map); + $map = array_slice($current_router_item['original_map'], 0, $count); + $item['original_map'] = $map; + if (isset($current_router_item['map'])) { + $item['map'] = array_slice($current_router_item['map'], 0, $count); + } + // Reset access to check it (for the first time). + unset($item['access']); + } + } + } + $item['href'] = implode('/', $map); + + // Skip links containing untranslated arguments. + if (strpos($item['href'], '%') !== FALSE) { + $item['access'] = FALSE; + return FALSE; + } + // menu_tree_check_access() may set this ahead of time for links to nodes. + if (!isset($item['access'])) { + if ($route = $item->getRoute()) { + $item['access'] = menu_item_route_access($route, $item['href'], $map); + } + elseif (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) { + // An error occurred loading an object. + $item['access'] = FALSE; + return FALSE; + } + // Apply the access check defined in hook_menu() if there is not route + // defined. + else { + _menu_check_access($item, $map); + } + } + // For performance, don't localize a link the user can't access. + if ($item['access']) { + _menu_item_localize($item, $map, TRUE); + } + } + + // Allow other customizations - e.g. adding a page-specific query string to the + // options array. For performance reasons we only invoke this hook if the link + // has the 'alter' flag set in the options array. + if (!empty($item['options']['alter'])) { + drupal_alter('translated_menu_link', $item, $map); + } + + return $map; +} + +/** + * 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 string $href + * Menu path as returned by $item['href'] of menu_get_item(). + * @param array $map + * An array of path arguments; for example, array('node', '5'). + * + * @return bool + * TRUE if the user has access or FALSE if the user should be presented + * with access denied. + * + * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException + * If the system path in $href does not match the $route. + */ +function menu_item_route_access(Route $route, $href, &$map) { + $request = RequestHelper::duplicate(\Drupal::request(), '/' . $href); + $request->attributes->set('_system_path', $href); + // Attempt to match this path to provide a fully built request to the + // access checker. + try { + $request->attributes->add(Drupal::service('router.dynamic')->matchRequest($request)); + } + catch (NotFoundHttpException $e) { + return FALSE; + } + + // Populate the map with any matching values from the request. + $path_bits = explode('/', trim($route->getPath(), '/')); + foreach ($map as $index => $map_item) { + $matches = array(); + // Search for placeholders wrapped by curly braces. For example, a path + // 'foo/{bar}/baz' would return 'bar'. + if (isset($path_bits[$index]) && preg_match('/{(?.*)}/', $path_bits[$index], $matches)) { + // If that placeholder is present on the request attributes, replace the + // placeholder in the map with the value. + if ($request->attributes->has($matches['placeholder'])) { + $map[$index] = $request->attributes->get($matches['placeholder']); + } + } + } + + return Drupal::service('access_manager')->check($route, $request); +} + +/** + * Gets a loaded object from a router item. + * + * menu_get_object() provides access to objects loaded by the current router + * item. For example, on the page node/%node, the router loads the %node object, + * and calling menu_get_object() will return that. Normally, it is necessary to + * specify the type of object referenced, however node is the default. + * The following example tests to see whether the node being displayed is of the + * "story" content type: + * @code + * $node = menu_get_object(); + * $story = $node->getType() == 'story'; + * @endcode + * + * @param $type + * Type of the object. These appear in hook_menu definitions as %type. Core + * provides aggregator_feed, aggregator_category, contact, filter_format, + * forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the + * relevant {$type}_load function for more on each. Defaults to node. + * @param $position + * The position of the object in the path, where the first path segment is 0. + * For node/%node, the position of %node is 1, but for comment/reply/%node, + * it's 2. Defaults to 1. + * @param $path + * See menu_get_item() for more on this. Defaults to the current path. + */ +function menu_get_object($type = 'node', $position = 1, $path = NULL) { + $router_item = menu_get_item($path); + if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') { + return $router_item['map'][$position]; + } +} + +/** + * Renders a menu tree based on the current path. + * + * The tree is expanded based on the current path and dynamic paths are also + * changed according to the defined to_arg functions (for example the 'My + * account' link is changed from user/% to a link with the current user's uid). + * + * @param $menu_name + * The name of the menu. + * + * @return + * A structured array representing the specified menu on the current page, to + * be rendered by drupal_render(). + */ +function menu_tree($menu_name) { + $menu_output = &drupal_static(__FUNCTION__, array()); + + if (!isset($menu_output[$menu_name])) { + $tree = menu_tree_page_data($menu_name); + $menu_output[$menu_name] = menu_tree_output($tree); + } + return $menu_output[$menu_name]; +} + +/** + * Returns a rendered menu tree. + * + * The menu item's LI element is given one of the following classes: + * - expanded: The menu item is showing its submenu. + * - collapsed: The menu item has a submenu which is not shown. + * - leaf: The menu item has no submenu. + * + * @param $tree + * A data structure representing the tree as returned from menu_tree_data. + * + * @return + * A structured array to be rendered by drupal_render(). + */ +function menu_tree_output($tree) { + $build = array(); + $items = array(); + + // Pull out just the menu links we are going to render so that we + // get an accurate count for the first/last classes. + foreach ($tree as $data) { + if ($data['link']['access'] && !$data['link']['hidden']) { + $items[] = $data; + } + } + + $router_item = menu_get_item(); + $num_items = count($items); + foreach ($items as $i => $data) { + $class = array(); + if ($i == 0) { + $class[] = 'first'; + } + if ($i == $num_items - 1) { + $class[] = 'last'; + } + // Set a class for the
  • -tag. Since $data['below'] may contain local + // tasks, only set 'expanded' class if the link also has children within + // the current menu. + if ($data['link']['has_children'] && $data['below']) { + $class[] = 'expanded'; + } + elseif ($data['link']['has_children']) { + $class[] = 'collapsed'; + } + else { + $class[] = 'leaf'; + } + // Set a class if the link is in the active trail. + if ($data['link']['in_active_trail']) { + $class[] = 'active-trail'; + $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; + } + // Normally, l() compares the href of every link with the current path and + // sets the active class accordingly. But local tasks do not appear in menu + // trees, so if the current path is a local task, and this link is its + // tab root, then we have to set the class manually. + if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != current_path()) { + $data['link']['localized_options']['attributes']['class'][] = 'active'; + } + + // Allow menu-specific theme overrides. + $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'); + $element['#attributes']['class'] = $class; + $element['#title'] = $data['link']['title']; + $element['#href'] = $data['link']['href']; + $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); + $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below']; + $element['#original_link'] = $data['link']; + // Index using the link's unique mlid. + $build[$data['link']['mlid']] = $element; + } + if ($build) { + // Make sure drupal_render() does not re-order the links. + $build['#sorted'] = TRUE; + // Add the theme wrapper for outer markup. + // Allow menu-specific theme overrides. + $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_'); + } + + return $build; +} + +/** + * Gets the data structure representing a named menu tree. + * + * Since this can be the full tree including hidden items, the data returned + * may be used for generating an an admin interface or a select. + * + * @param $menu_name + * The named menu links to return + * @param $link + * A fully loaded menu link, or NULL. If a link is supplied, only the + * path to root will be included in the returned tree - as if this link + * represented the current page in a visible menu. + * @param $max_depth + * Optional maximum depth of links to retrieve. Typically useful if only one + * or two levels of a sub tree are needed in conjunction with a non-NULL + * $link, in which case $max_depth should be greater than $link['depth']. + * + * @return + * An tree of menu links in an array, in the order they should be rendered. + */ +function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { + $tree = &drupal_static(__FUNCTION__, array()); + $language_interface = language(Language::TYPE_INTERFACE); + + // Use $mlid as a flag for whether the data being loaded is for the whole tree. + $mlid = isset($link['mlid']) ? $link['mlid'] : 0; + // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth. + $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth; + + if (!isset($tree[$cid])) { + // If the static variable doesn't have the data, check {cache_menu}. + $cache = cache('menu')->get($cid); + if ($cache && isset($cache->data)) { + // If the cache entry exists, it contains the parameters for + // menu_build_tree(). + $tree_parameters = $cache->data; + } + // If the tree data was not in the cache, build $tree_parameters. + if (!isset($tree_parameters)) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + if ($mlid) { + // The tree is for a single item, so we need to match the values in its + // p columns and 0 (the top level) with the plid values of other links. + $parents = array(0); + for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { + if (!empty($link["p$i"])) { + $parents[] = $link["p$i"]; + } + } + $tree_parameters['expanded'] = $parents; + $tree_parameters['active_trail'] = $parents; + $tree_parameters['active_trail'][] = $mlid; + } + + // Cache the tree building parameters using the page-specific cid. + cache('menu')->set($cid, $tree_parameters, CacheBackendInterface::CACHE_PERMANENT, array('menu' => $menu_name)); + } + + // Build the tree using the parameters; the resulting tree will be cached + // by _menu_build_tree()). + $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); + } + + return $tree[$cid]; +} + +/** + * Sets the path for determining the active trail of the specified menu tree. + * + * This path will also affect the breadcrumbs under some circumstances. + * Breadcrumbs are built using the preferred link returned by + * menu_link_get_preferred(). If the preferred link is inside one of the menus + * specified in calls to menu_tree_set_path(), the preferred link will be + * overridden by the corresponding path returned by menu_tree_get_path(). + * + * Setting this path does not affect the main content; for that use + * menu_set_active_item() instead. + * + * @param $menu_name + * The name of the affected menu tree. + * @param $path + * The path to use when finding the active trail. + */ +function menu_tree_set_path($menu_name, $path = NULL) { + $paths = &drupal_static(__FUNCTION__); + if (isset($path)) { + $paths[$menu_name] = $path; + } + return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL; +} + +/** + * Gets the path for determining the active trail of the specified menu tree. + * + * @param $menu_name + * The menu name of the requested tree. + * + * @return + * A string containing the path. If no path has been specified with + * menu_tree_set_path(), NULL is returned. + */ +function menu_tree_get_path($menu_name) { + return menu_tree_set_path($menu_name); +} + +/** + * Gets the data structure for a named menu tree, based on the current page. + * + * The tree order is maintained by storing each parent in an individual + * field, see http://drupal.org/node/141866 for more. + * + * @param $menu_name + * The named menu links to return. + * @param $max_depth + * (optional) The maximum depth of links to retrieve. + * @param $only_active_trail + * (optional) Whether to only return the links in the active trail (TRUE) + * instead of all links on every level of the menu link tree (FALSE). Defaults + * to FALSE. Internally used for breadcrumbs only. + * + * @return + * An array of menu links, in the order they should be rendered. The array + * is a list of associative arrays -- these have two keys, link and below. + * link is a menu item, ready for theming as a link. Below represents the + * submenu below the link if there is one, and it is a subtree that has the + * same structure described for the top-level array. + */ +function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) { + $tree = &drupal_static(__FUNCTION__, array()); + + $language_interface = language(Language::TYPE_INTERFACE); + + // Check if the active trail has been overridden for this menu tree. + $active_path = menu_tree_get_path($menu_name); + // Load the router item corresponding to the current page. + $request = \Drupal::request(); + $system_path = NULL; + if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { + // @todo https://drupal.org/node/2068471 is adding support so we can tell + // if this is called on a 404/403 page. + $system_path = $request->attributes->get('_system_path'); + $page_not_403 = 1; + } + // @todo Remove once the old router system is removed. + elseif ($request->attributes->has('_legacy')) { + // Page is a 404 if no item is loaded. + if ($item = menu_get_item($active_path)) { + $system_path = $item['href']; + // Variable to indicate that we are not serving an access denied response + // for this page. Used to limit the number of links rendered. + $page_not_403 = (int) $item['access']; + } + } + if (isset($system_path)) { + if (isset($max_depth)) { + $max_depth = min($max_depth, MENU_MAX_DEPTH); + } + // Generate a cache ID (cid) specific for this page. + $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth; + // If we are asked for the active trail only, and $menu_name has not been + // built and cached for this page yet, then this likely means that it + // won't be built anymore, as this function is invoked from + // template_preprocess_page(). So in order to not build a giant menu tree + // that needs to be checked for access on all levels, we simply check + // whether we have the menu already in cache, or otherwise, build a minimum + // tree containing the breadcrumb/active trail only. + // @see menu_set_active_trail() + if (!isset($tree[$cid]) && $only_active_trail) { + $cid .= ':trail'; + } + + if (!isset($tree[$cid])) { + // If the static variable doesn't have the data, check {cache_menu}. + $cache = cache('menu')->get($cid); + if ($cache && isset($cache->data)) { + // If the cache entry exists, it contains the parameters for + // menu_build_tree(). + $tree_parameters = $cache->data; + } + // If the tree data was not in the cache, build $tree_parameters. + if (!isset($tree_parameters)) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + // Parent mlids; used both as key and value to ensure uniqueness. + // We always want all the top-level links with plid == 0. + $active_trail = array(0 => 0); + + // If this page is accessible to the current user, build the tree + // parameters accordingly. + if ($page_not_403) { + // Find a menu link corresponding to the current path. If $active_path + // is NULL, let menu_link_get_preferred() determine the path. + if ($active_link = menu_link_get_preferred($active_path, $menu_name)) { + // The active link may only be taken into account to build the + // active trail, if it resides in the requested menu. Otherwise, + // we'd needlessly re-run _menu_build_tree() queries for every menu + // on every page. + if ($active_link['menu_name'] == $menu_name) { + // Use all the coordinates, except the last one because there + // can be no child beyond the last column. + for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { + if ($active_link['p' . $i]) { + $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; + } + } + // If we are asked to build links for the active trail only, skip + // the entire 'expanded' handling. + if ($only_active_trail) { + $tree_parameters['only_active_trail'] = TRUE; + } + } + } + $parents = $active_trail; + + $expanded = Drupal::state()->get('menu_expanded'); + // Check whether the current menu has any links set to be expanded. + if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) { + // Collect all the links set to be expanded, and then add all of + // their children to the list as well. + do { + $query = Drupal::entityQuery('menu_link') + ->condition('menu_name', $menu_name) + ->condition('expanded', 1) + ->condition('has_children', 1) + ->condition('plid', $parents, 'IN') + ->condition('mlid', $parents, 'NOT IN'); + $result = $query->execute(); + $parents += $result; + } while (!empty($result)); + } + $tree_parameters['expanded'] = $parents; + $tree_parameters['active_trail'] = $active_trail; + } + // If access is denied, we only show top-level links in menus. + else { + $tree_parameters['expanded'] = $active_trail; + $tree_parameters['active_trail'] = $active_trail; + } + // Cache the tree building parameters using the page-specific cid. + cache('menu')->set($cid, $tree_parameters, CacheBackendInterface::CACHE_PERMANENT, array('menu' => $menu_name)); + } + + // Build the tree using the parameters; the resulting tree will be cached + // by _menu_build_tree(). + $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); + } + return $tree[$cid]; + } + + return array(); +} + +/** + * Builds a menu tree, translates links, and checks access. + * + * @param $menu_name + * The name of the menu. + * @param $parameters + * (optional) An associative array of build parameters. Possible keys: + * - expanded: An array of parent link ids to return only menu links that are + * children of one of the plids in this list. If empty, the whole menu tree + * is built, unless 'only_active_trail' is TRUE. + * - active_trail: An array of mlids, representing the coordinates of the + * currently active menu link. + * - only_active_trail: Whether to only return links that are in the active + * trail. This option is ignored, if 'expanded' is non-empty. Internally + * used for breadcrumbs. + * - min_depth: The minimum depth of menu links in the resulting tree. + * Defaults to 1, which is the default to build a whole tree for a menu + * (excluding menu container itself). + * - max_depth: The maximum depth of menu links in the resulting tree. + * - conditions: An associative array of custom database select query + * condition key/value pairs; see _menu_build_tree() for the actual query. + * + * @return + * A fully built menu tree. + */ +function menu_build_tree($menu_name, array $parameters = array()) { + // Build the menu tree. + $data = _menu_build_tree($menu_name, $parameters); + // Check access for the current user to each item in the tree. + menu_tree_check_access($data['tree'], $data['node_links']); + return $data['tree']; +} + +/** + * Builds a menu tree. + * + * This function may be used build the data for a menu tree only, for example + * to further massage the data manually before further processing happens. + * menu_tree_check_access() needs to be invoked afterwards. + * + * @see menu_build_tree() + */ +function _menu_build_tree($menu_name, array $parameters = array()) { + // Static cache of already built menu trees. + $trees = &drupal_static(__FUNCTION__, array()); + $language_interface = language(Language::TYPE_INTERFACE); + + // Build the cache id; sort parents to prevent duplicate storage and remove + // default parameter values. + if (isset($parameters['expanded'])) { + sort($parameters['expanded']); + } + $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters)); + + // If we do not have this tree in the static cache, check {cache_menu}. + if (!isset($trees[$tree_cid])) { + $cache = cache('menu')->get($tree_cid); + if ($cache && isset($cache->data)) { + $trees[$tree_cid] = $cache->data; + } + } + + if (!isset($trees[$tree_cid])) { + $query = Drupal::entityQuery('menu_link'); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $query->sort('p' . $i, 'ASC'); + } + $query->condition('menu_name', $menu_name); + if (!empty($parameters['expanded'])) { + $query->condition('plid', $parameters['expanded'], 'IN'); + } + elseif (!empty($parameters['only_active_trail'])) { + $query->condition('mlid', $parameters['active_trail'], 'IN'); + } + $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); + if ($min_depth != 1) { + $query->condition('depth', $min_depth, '>='); + } + if (isset($parameters['max_depth'])) { + $query->condition('depth', $parameters['max_depth'], '<='); + } + // Add custom query conditions, if any were passed. + if (isset($parameters['conditions'])) { + foreach ($parameters['conditions'] as $column => $value) { + $query->condition($column, $value); + } + } + + // Build an ordered array of links using the query result object. + $links = array(); + if ($result = $query->execute()) { + $links = menu_link_load_multiple($result); + } + $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); + $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); + $data['node_links'] = array(); + menu_tree_collect_node_links($data['tree'], $data['node_links']); + + // Cache the data, if it is not already in the cache. + cache('menu')->set($tree_cid, $data, CacheBackendInterface::CACHE_PERMANENT, array('menu' => $menu_name)); + $trees[$tree_cid] = $data; + } + + return $trees[$tree_cid]; +} + +/** + * Collects node links from a given menu tree recursively. + * + * @param $tree + * The menu tree you wish to collect node links from. + * @param $node_links + * An array in which to store the collected node links. + */ +function menu_tree_collect_node_links(&$tree, &$node_links) { + foreach ($tree as $key => $v) { + if ($tree[$key]['link']['router_path'] == 'node/%') { + $nid = substr($tree[$key]['link']['link_path'], 5); + if (is_numeric($nid)) { + $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link']; + $tree[$key]['link']['access'] = FALSE; + } + } + if ($tree[$key]['below']) { + menu_tree_collect_node_links($tree[$key]['below'], $node_links); + } + } +} + +/** + * Checks access and performs dynamic operations for each link in the tree. + * + * @param $tree + * The menu tree you wish to operate on. + * @param $node_links + * A collection of node link references generated from $tree by + * menu_tree_collect_node_links(). + */ +function menu_tree_check_access(&$tree, $node_links = array()) { + if ($node_links) { + $nids = array_keys($node_links); + $select = db_select('node_field_data', 'n'); + $select->addField('n', 'nid'); + // @todo This should be actually filtering on the desired node status field + // language and just fall back to the default language. + $select->condition('n.status', 1); + + $select->condition('n.nid', $nids, 'IN'); + $select->addTag('node_access'); + $nids = $select->execute()->fetchCol(); + foreach ($nids as $nid) { + foreach ($node_links[$nid] as $mlid => $link) { + $node_links[$nid][$mlid]['access'] = TRUE; + } + } + } + _menu_tree_check_access($tree); +} + +/** + * Sorts the menu tree and recursively checks access for each item. + */ +function _menu_tree_check_access(&$tree) { + $new_tree = array(); + foreach ($tree as $key => $v) { + $item = &$tree[$key]['link']; + _menu_link_translate($item); + if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { + if ($tree[$key]['below']) { + _menu_tree_check_access($tree[$key]['below']); + } + // The weights are made a uniform 5 digits by adding 50000 as an offset. + // After _menu_link_translate(), $item['title'] has the localized link title. + // Adding the mlid to the end of the index insures that it is unique. + $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key]; + } + } + // Sort siblings in the tree based on the weights and localized titles. + ksort($new_tree); + $tree = $new_tree; +} + +/** + * Sorts and returns the built data representing a menu tree. + * + * @param $links + * A flat array of menu links that are part of the menu. Each array element + * is an associative array of information about the menu link, containing the + * fields from the {menu_links} table, and optionally additional information + * from the {menu_router} table, if the menu item appears in both tables. + * This array must be ordered depth-first. See _menu_build_tree() for a sample + * query. + * @param $parents + * An array of the menu link ID values that are in the path from the current + * page to the root of the menu tree. + * @param $depth + * The minimum depth to include in the returned menu tree. + * + * @return + * An array of menu links in the form of a tree. Each item in the tree is an + * associative array containing: + * - link: The menu link item from $links, with additional element + * 'in_active_trail' (TRUE if the link ID was in $parents). + * - below: An array containing the sub-tree of this item, where each element + * is a tree item array with 'link' and 'below' elements. This array will be + * empty if the menu item has no items in its sub-tree having a depth + * greater than or equal to $depth. + */ +function menu_tree_data(array $links, array $parents = array(), $depth = 1) { + // Reverse the array so we can use the more efficient array_pop() function. + $links = array_reverse($links); + return _menu_tree_data($links, $parents, $depth); +} + +/** + * Builds the data representing a menu tree. + * + * The function is a bit complex because the rendering of a link depends on + * the next menu link. + */ +function _menu_tree_data(&$links, $parents, $depth) { + $tree = array(); + while ($item = array_pop($links)) { + // We need to determine if we're on the path to root so we can later build + // the correct active trail and breadcrumb. + $item['in_active_trail'] = in_array($item['mlid'], $parents); + // Add the current link to the tree. + $tree[$item['mlid']] = array( + 'link' => $item, + 'below' => array(), + ); + // Look ahead to the next link, but leave it on the array so it's available + // to other recursive function calls if we return or build a sub-tree. + $next = end($links); + // Check whether the next link is the first in a new sub-tree. + if ($next && $next['depth'] > $depth) { + // Recursively call _menu_tree_data to build the sub-tree. + $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']); + // Fetch next link after filling the sub-tree. + $next = end($links); + } + // Determine if we should exit the loop and return. + if (!$next || $next['depth'] < $depth) { + break; + } + } + return $tree; +} + +/** + * Implements template_preprocess_HOOK() for theme_menu_tree(). + */ +function template_preprocess_menu_tree(&$variables) { + $variables['tree'] = $variables['tree']['#children']; +} + +/** + * Returns HTML for a wrapper for a menu sub-tree. + * + * @param $variables + * An associative array containing: + * - tree: An HTML string containing the tree's items. + * + * @see template_preprocess_menu_tree() + * @ingroup themeable + */ +function theme_menu_tree($variables) { + return ''; +} + +/** + * Returns HTML for a menu link and submenu. + * + * @param $variables + * An associative array containing: + * - element: Structured array data for a menu link. + * + * @ingroup themeable + */ +function theme_menu_link(array $variables) { + $element = $variables['element']; + $sub_menu = ''; + + if ($element['#below']) { + $sub_menu = drupal_render($element['#below']); + } + $output = l($element['#title'], $element['#href'], $element['#localized_options']); + return '' . $output . $sub_menu . "
  • \n"; +} + +/** + * Returns HTML for a single local task link. + * + * @param $variables + * An associative array containing: + * - element: A render element containing: + * - #link: A menu link array with 'title', 'href', and 'localized_options' + * keys. + * - #active: A boolean indicating whether the local task is active. + * + * @ingroup themeable + */ +function theme_menu_local_task($variables) { + $link = $variables['element']['#link']; + $link += array( + 'localized_options' => array(), + ); + $link_text = $link['title']; + + if (!empty($variables['element']['#active'])) { + // Add text to indicate active tab for non-visual users. + $active = '' . t('(active tab)') . ''; + + // If the link does not contain HTML already, check_plain() it now. + // After we set 'html'=TRUE the link will not be sanitized by l(). + if (empty($link['localized_options']['html'])) { + $link['title'] = check_plain($link['title']); + } + $link['localized_options']['html'] = TRUE; + $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active)); + } + + return '' . l($link_text, $link['href'], $link['localized_options']) . ''; +} + +/** + * Returns HTML for a single local action link. + * + * @param $variables + * An associative array containing: + * - element: A render element containing: + * - #link: A menu link array with 'title', 'href', and 'localized_options' + * keys. + * + * @ingroup themeable + */ +function theme_menu_local_action($variables) { + $link = $variables['element']['#link']; + $link += array( + 'href' => '', + 'localized_options' => array(), + ); + $link['localized_options']['attributes']['class'][] = 'button'; + $link['localized_options']['attributes']['class'][] = 'button-action'; + + $output = '
  • '; + $output .= l($link['title'], $link['href'], $link['localized_options']); + $output .= "
  • "; + + return $output; +} + +/** + * Generates elements for the $arg array in the help hook. + */ +function drupal_help_arg($arg = array()) { + // Note - the number of empty elements should be > MENU_MAX_PARTS. + return $arg + array('', '', '', '', '', '', '', '', '', '', '', ''); +} + +/** + * Returns the help associated with the active menu item. + */ +function menu_get_active_help() { + $output = ''; + $router_path = menu_tab_root_path(); + // We will always have a path unless we are on a 403 or 404. + if (!$router_path) { + return ''; + } + + $arg = drupal_help_arg(arg(NULL)); + + foreach (Drupal::moduleHandler()->getImplementations('help') as $module) { + $function = $module . '_help'; + // Lookup help for this path. + if ($help = $function($router_path, $arg)) { + $output .= $help . "\n"; + } + } + return $output; +} + +/** + * Gets the custom theme for the current page, if there is one. + * + * @param $initialize + * This parameter should only be used internally; it is set to TRUE in order + * to force the custom theme to be initialized for the current page request. + * + * @return + * The machine-readable name of the custom theme, if there is one. + * + * @see menu_set_custom_theme() + */ +function menu_get_custom_theme($initialize = FALSE) { + $custom_theme = &drupal_static(__FUNCTION__); + // Skip this if the site is offline or being installed or updated, since the + // menu system may not be correctly initialized then. + if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) { + // First allow modules to dynamically set a custom theme for the current + // page. Since we can only have one, the last module to return a valid + // theme takes precedence. + $custom_themes = array_filter(Drupal::moduleHandler()->invokeAll('custom_theme'), 'drupal_theme_access'); + if (!empty($custom_themes)) { + $custom_theme = array_pop($custom_themes); + } + // If there is a theme callback function for the current page, execute it. + // If this returns a valid theme, it will override any theme that was set + // by a hook_custom_theme() implementation above. + $router_item = menu_get_item(); + if (!empty($router_item['access']) && !empty($router_item['theme_callback'])) { + $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']); + if (drupal_theme_access($theme_name)) { + $custom_theme = $theme_name; + } + } + } + return $custom_theme; +} + +/** + * Sets a custom theme for the current page, if there is one. + */ +function menu_set_custom_theme() { + menu_get_custom_theme(TRUE); +} + +/** + * Returns an array containing the names of system-defined (default) menus. + */ +function menu_list_system_menus() { + return array( + 'tools' => 'Tools', + 'admin' => 'Administration', + 'account' => 'User account menu', + 'main' => 'Main navigation', + 'footer' => 'Footer menu', + ); +} + +/** + * Returns an array of links to be rendered as the Main menu. + */ +function menu_main_menu() { + $config = Drupal::config('menu.settings'); + $menu_enabled = module_exists('menu'); + // When menu module is not enabled, we need a hardcoded default value. + $main_links_source = $menu_enabled ? $config->get('main_links') : 'main'; + return menu_navigation_links($main_links_source); +} + +/** + * Returns an array of links to be rendered as the Secondary links. + */ +function menu_secondary_menu() { + $config = Drupal::config('menu.settings'); + $menu_enabled = module_exists('menu'); + // When menu module is not enabled, we need a hardcoded default value. + $main_links_source = $menu_enabled ? $config->get('main_links') : 'main'; + $secondary_links_source = $menu_enabled ? $config->get('secondary_links') : 'account'; + + // If the secondary menu source is set as the primary menu, we display the + // second level of the primary menu. + if ($secondary_links_source == $main_links_source) { + return menu_navigation_links($main_links_source, 1); + } + else { + return menu_navigation_links($secondary_links_source, 0); + } +} + +/** + * Returns an array of links for a navigation menu. + * + * @param $menu_name + * The name of the menu. + * @param $level + * Optional, the depth of the menu to be returned. + * + * @return + * An array of links of the specified menu and level. + */ +function menu_navigation_links($menu_name, $level = 0) { + // Don't even bother querying the menu table if no menu is specified. + if (empty($menu_name)) { + return array(); + } + + // Get the menu hierarchy for the current page. + $tree = menu_tree_page_data($menu_name, $level + 1); + + // Go down the active trail until the right level is reached. + while ($level-- > 0 && $tree) { + // Loop through the current level's items until we find one that is in trail. + while ($item = array_shift($tree)) { + if ($item['link']['in_active_trail']) { + // If the item is in the active trail, we continue in the subtree. + $tree = empty($item['below']) ? array() : $item['below']; + break; + } + } + } + + // Create a single level of links. + $router_item = menu_get_item(); + $links = array(); + foreach ($tree as $item) { + if (!$item['link']['hidden']) { + $class = ''; + $l = $item['link']['localized_options']; + $l['href'] = $item['link']['href']; + $l['title'] = $item['link']['title']; + if ($item['link']['in_active_trail']) { + $class = ' active-trail'; + $l['attributes']['class'][] = 'active-trail'; + } + // Normally, l() compares the href of every link with the current path and + // sets the active class accordingly. But local tasks do not appear in + // menu trees, so if the current path is a local task, and this link is + // its tab root, then we have to set the class manually. + if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != current_path()) { + $l['attributes']['class'][] = 'active'; + } + // Keyed with the unique mlid to generate classes in theme_links(). + $links['menu-' . $item['link']['mlid'] . $class] = $l; + } + } + return $links; +} + +/** + * Collects the local tasks (tabs), action links, and the root path. + * + * @param $level + * The level of tasks you ask for. Primary tasks are 0, secondary are 1. + * + * @return + * An array containing + * - tabs: Local tasks for the requested level. + * - actions: Action links for the requested level. + * - root_path: The router path for the current page. If the current page is + * a default local task, then this corresponds to the parent tab. + * + * @see hook_menu_local_tasks() + * @see hook_menu_local_tasks_alter() + */ +function menu_local_tasks($level = 0) { + $data = &drupal_static(__FUNCTION__); + $root_path = &drupal_static(__FUNCTION__ . ':root_path', ''); + $empty = array( + 'tabs' => array(), + 'actions' => array(), + 'root_path' => &$root_path, + ); + + if (!isset($data)) { + $data = array(); + // Set defaults in case there are no actions or tabs. + $actions = $empty['actions']; + $tabs = array(); + + $router_item = menu_get_item(); + + // If this router item points to its parent, start from the parents to + // compute tabs and actions. + if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) { + $router_item = menu_get_item($router_item['tab_parent_href']); + } + + // If we failed to fetch a router item or the current user doesn't have + // access to it, don't bother computing the tabs. + if (!$router_item || !$router_item['access']) { + return $empty; + } + // @todo remove all code using {menu_router} and anything using MENU_* + // constants when all local actions and local tasks are converted to + // plugins. The remaining code should just invoke those managers plus do the + // invocations of hook_menu_local_tasks() and hook_menu_local_tasks_alter(). + + // Get all tabs (also known as local tasks) and the root page. + $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('menu_router') + ->condition('tab_root', $router_item['tab_root']) + ->condition('context', MENU_CONTEXT_INLINE, '<>') + ->orderBy('weight') + ->orderBy('title') + ->execute(); + $map = $router_item['original_map']; + $children = array(); + $tasks = array(); + $root_path = $router_item['path']; + + foreach ($result as $item) { + _menu_translate($item, $map, TRUE); + if ($item['tab_parent']) { + // All tabs, but not the root page. + $children[$item['tab_parent']][$item['path']] = $item; + } + // Store the translated item for later use. + $tasks[$item['path']] = $item; + } + + // Find all tabs below the current path. + $path = $router_item['path']; + // Tab parenting may skip levels, so the number of parts in the path may not + // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort). + $depth = 1001; + $actions = array(); + while (isset($children[$path])) { + $tabs_current = array(); + $actions_current = array(); + $next_path = ''; + $tab_count = 0; + $action_count = 0; + foreach ($children[$path] as $item) { + // Local tasks can be normal items too, so bitmask with + // MENU_IS_LOCAL_TASK before checking. + if (!($item['type'] & MENU_IS_LOCAL_TASK)) { + // This item is not a tab, skip it. + continue; + } + if ($item['access']) { + $link = $item; + // The default task is always active. As tabs can be normal items + // too, so bitmask with MENU_LINKS_TO_PARENT before checking. + if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { + // Find the first parent which is not a default local task or action. + for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']); + // Use the path of the parent instead. + $link['href'] = $tasks[$p]['href']; + // Mark the link as active, if the current path happens to be the + // path of the default local task itself (i.e., instead of its + // tab_parent_href or tab_root_href). Normally, links for default + // local tasks link to their parent, but the path of default local + // tasks can still be accessed directly, in which case this link + // would not be marked as active, since l() only compares the href + // with current_path(). + if ($link['href'] != current_path()) { + $link['localized_options']['attributes']['class'][] = 'active'; + } + $tabs_current[$link['href']] = array( + '#theme' => 'menu_local_task', + '#link' => $link, + '#active' => TRUE, + '#weight' => isset($link['weight']) ? $link['weight'] : NULL, + ); + $next_path = $item['path']; + $tab_count++; + } + else { + // Actions can be normal items too, so bitmask with + // MENU_IS_LOCAL_ACTION before checking. + if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) { + // The item is an action, display it as such. + $actions_current[$link['href']] = array( + '#theme' => 'menu_local_action', + '#link' => $link, + '#weight' => isset($link['weight']) ? $link['weight'] : NULL, + ); + $action_count++; + } + else { + // Otherwise, it's a normal tab. + $tabs_current[$link['href']] = array( + '#theme' => 'menu_local_task', + '#link' => $link, + '#weight' => isset($link['weight']) ? $link['weight'] : NULL, + ); + $tab_count++; + } + } + } + } + $path = $next_path; + $tabs[$depth] = $tabs_current; + $actions = array_merge($actions, $actions_current); + $depth++; + } + $data['actions'] = $actions; + // Find all tabs at the same level or above the current one. + $parent = $router_item['tab_parent']; + $path = $router_item['path']; + $current = $router_item; + $depth = 1000; + while (isset($children[$parent])) { + $tabs_current = array(); + $next_path = ''; + $next_parent = ''; + $count = 0; + foreach ($children[$parent] as $item) { + // Skip local actions. + if ($item['type'] & MENU_IS_LOCAL_ACTION) { + continue; + } + if ($item['access']) { + $count++; + $link = $item; + // Local tasks can be normal items too, so bitmask with + // MENU_LINKS_TO_PARENT before checking. + if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { + // Find the first parent which is not a default local task. + for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']); + // Use the path of the parent instead. + $link['href'] = $tasks[$p]['href']; + if ($item['path'] == $router_item['path']) { + $root_path = $tasks[$p]['path']; + } + } + // We check for the active tab. + if ($item['path'] == $path) { + // Mark the link as active, if the current path is a (second-level) + // local task of a default local task. Since this default local task + // links to its parent, l() will not mark it as active, as it only + // compares the link's href to current_path(). + if ($link['href'] != current_path()) { + $link['localized_options']['attributes']['class'][] = 'active'; + } + $tabs_current[$link['href']] = array( + '#theme' => 'menu_local_task', + '#link' => $link, + '#active' => TRUE, + '#weight' => isset($link['weight']) ? $link['weight'] : NULL, + ); + $next_path = $item['tab_parent']; + if (isset($tasks[$next_path])) { + $next_parent = $tasks[$next_path]['tab_parent']; + } + } + else { + $tabs_current[$link['href']] = array( + '#theme' => 'menu_local_task', + '#link' => $link, + '#weight' => isset($link['weight']) ? $link['weight'] : NULL, + ); + } + } + } + $path = $next_path; + $parent = $next_parent; + $tabs[$depth] = $tabs_current; + $depth--; + } + // Sort by depth. + ksort($tabs); + // Remove the depth, we are interested only in their relative placement. + $tabs = array_values($tabs); + $data['tabs'] = $tabs; + // Look for route-based tabs. + $route_name = Drupal::request()->attributes->get('_route'); + if (!empty($route_name)) { + $manager = Drupal::service('plugin.manager.menu.local_task'); + $local_tasks = $manager->getTasksBuild($route_name); + foreach ($local_tasks as $level => $items) { + $data['tabs'][$level] = empty($data['tabs'][$level]) ? $items : array_merge($data['tabs'][$level], $items); + } + } + + // Allow modules to dynamically add further tasks. + $module_handler = Drupal::moduleHandler(); + foreach ($module_handler->getImplementations('menu_local_tasks') as $module) { + $function = $module . '_menu_local_tasks'; + $function($data, $router_item, $root_path); + } + // Allow modules to alter local tasks. + $module_handler->alter('menu_local_tasks', $data, $router_item, $root_path); + } + + if (isset($data['tabs'][$level])) { + return array( + 'tabs' => $data['tabs'][$level], + 'actions' => $data['actions'], + 'root_path' => $root_path, + ); + } + elseif (!empty($data['actions'])) { + return array('actions' => $data['actions']) + $empty; + } + return $empty; +} + +/** + * Retrieves contextual links for a path based on registered local tasks. + * + * This leverages the menu system to retrieve the first layer of registered + * local tasks for a given system path. All local tasks of the tab type + * MENU_CONTEXT_INLINE are taken into account. + * + * For example, when considering the following registered local tasks: + * - node/%node/view (default local task) with no 'context' defined + * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE + * - node/%node/revisions with context: MENU_CONTEXT_PAGE + * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE + * + * If the path "node/123" is passed to this function, then it will return the + * links for 'edit' and 'report-as-spam'. + * + * @param $module + * The name of the implementing module. This is used to prefix the key for + * each contextual link, which is transformed into a CSS class during + * rendering by theme_links(). For example, if $module is 'block' and the + * retrieved local task path argument is 'edit', then the resulting CSS class + * will be 'block-edit'. + * @param $parent_path + * The static menu router path of the object to retrieve local tasks for, for + * example 'node' or 'admin/structure/block/manage'. + * @param $args + * A list of dynamic path arguments to append to $parent_path to form the + * fully-qualified menu router path; for example, array(123) for a certain + * node or array('system', 'tools') for a certain block. + * + * @return + * A list of menu router items that are local tasks for the passed-in path. + * + * @see contextual_links_preprocess() + * @see hook_menu() + */ +function menu_contextual_links($module, $parent_path, $args) { + static $path_empty = array(); + + $links = array(); + // Performance: In case a previous invocation for the same parent path did not + // return any links, we immediately return here. + if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) { + return $links; + } + // Construct the item-specific parent path. + $path = $parent_path . '/' . implode('/', $args); + + // Get the router item for the given parent link path. + $router_item = menu_get_item($path); + if (!$router_item || !$router_item['access']) { + $path_empty[$parent_path] = TRUE; + return $links; + } + $data = &drupal_static(__FUNCTION__, array()); + $root_path = $router_item['path']; + + // Performance: For a single, normalized path (such as 'node/%') we only query + // available tasks once per request. + if (!isset($data[$root_path])) { + // Get all contextual links that are direct children of the router item and + // not of the tab type 'view'. + $data[$root_path] = db_select('menu_router', 'm') + ->fields('m') + ->condition('tab_parent', $router_item['tab_root']) + ->condition('context', MENU_CONTEXT_NONE, '<>') + ->condition('context', MENU_CONTEXT_PAGE, '<>') + ->orderBy('weight') + ->orderBy('title') + ->execute() + ->fetchAllAssoc('path', PDO::FETCH_ASSOC); + } + $parent_length = drupal_strlen($root_path) + 1; + $map = $router_item['original_map']; + foreach ($data[$root_path] as $item) { + // Extract the actual "task" string from the path argument. + $key = drupal_substr($item['path'], $parent_length); + + // Denormalize and translate the contextual link. + _menu_translate($item, $map, TRUE); + if (!$item['access']) { + continue; + } + + // If this item is a default local task, rewrite the href to link to its + // parent item. + if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { + $item['href'] = $item['tab_parent_href']; + } + + // All contextual links are keyed by the actual "task" path argument, + // prefixed with the name of the implementing module. + $links[$module . '-' . $key] = $item; + } + + // Allow modules to alter contextual links. + drupal_alter('menu_contextual_links', $links, $router_item, $root_path); + + // Performance: If the current user does not have access to any links for this + // router path and no other module added further links, we assign FALSE here + // to skip the entire process the next time the same router path is requested. + if (empty($links)) { + $path_empty[$parent_path] = TRUE; + } + + return $links; +} + +/** + * Returns the rendered local tasks at the top level. + */ +function menu_primary_local_tasks() { + $links = menu_local_tasks(0); + // Do not display single tabs. + return count(element_get_visible_children($links['tabs'])) > 1 ? $links['tabs'] : ''; +} + +/** + * Returns the rendered local tasks at the second level. + */ +function menu_secondary_local_tasks() { + $links = menu_local_tasks(1); + // Do not display single tabs. + return count(element_get_visible_children($links['tabs'])) > 1 ? $links['tabs'] : ''; +} + +/** + * Returns the rendered local actions at the current level. + */ +function menu_get_local_actions() { + $links = menu_local_tasks(); + $router_item = menu_get_item(); + $manager = Drupal::service('plugin.manager.menu.local_action'); + $local_actions = $manager->getActionsForRoute($router_item['route_name']); + foreach ($local_actions as $plugin) { + $route_path = $manager->getPath($plugin); + $action_router_item = menu_get_item($route_path); + $links['actions'][$route_path] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'title' => $manager->getTitle($plugin), + 'href' => $route_path, + ), + '#access' => $action_router_item['access'], + ); + } + uasort($links['actions'], 'element_sort'); + return $links['actions']; +} + +/** + * Returns the router path, or the path for a default local task's parent. + */ +function menu_tab_root_path() { + $links = menu_local_tasks(); + return $links['root_path']; +} + +/** + * Returns a renderable element for the primary and secondary tabs. + */ +function menu_local_tabs() { + $build = array( + '#theme' => 'menu_local_tasks', + '#primary' => menu_primary_local_tasks(), + '#secondary' => menu_secondary_local_tasks(), + ); + return !empty($build['#primary']) || !empty($build['#secondary']) ? $build : array(); +} + +/** + * Returns HTML for primary and secondary local tasks. + * + * @param $variables + * An associative array containing: + * - primary: (optional) An array of local tasks (tabs). + * - secondary: (optional) An array of local tasks (tabs). + * + * @ingroup themeable + * @see menu_local_tasks() + */ +function theme_menu_local_tasks(&$variables) { + $output = ''; + + if (!empty($variables['primary'])) { + $variables['primary']['#prefix'] = '

    ' . t('Primary tabs') . '

    '; + $variables['primary']['#prefix'] .= ''; + $output .= drupal_render($variables['primary']); + } + if (!empty($variables['secondary'])) { + $variables['secondary']['#prefix'] = '

    ' . t('Secondary tabs') . '

    '; + $variables['secondary']['#prefix'] .= ''; + $output .= drupal_render($variables['secondary']); + } + + return $output; +} + +/** + * Sets (or gets) the active menu for the current page. + * + * The active menu for the page determines the active trail. + * + * @return + * An array of menu machine names, in order of preference. The + * 'system.menu.active_menus_default' config item may be used to assert a menu + * order different from the order of creation, or to prevent a particular menu + * from being used at all in the active trail. + * E.g., $conf['system.menu']['active_menus_default'] = array('tools', + * 'main'). + */ +function menu_set_active_menu_names($menu_names = NULL) { + $active = &drupal_static(__FUNCTION__); + + if (isset($menu_names) && is_array($menu_names)) { + $active = $menu_names; + } + elseif (!isset($active)) { + $config = Drupal::config('system.menu'); + $active = $config->get('active_menus_default') ?: array_keys(menu_list_system_menus()); + } + return $active; +} + +/** + * Gets the active menu for the current page. + */ +function menu_get_active_menu_names() { + return menu_set_active_menu_names(); +} + +/** + * Sets the active path, which determines which page is loaded. + * + * Note that this may not have the desired effect unless invoked very early + * in the page load or unless you do a subrequest to generate your page output. + * + * @param $path + * A Drupal path - not a path alias. + */ +function menu_set_active_item($path) { + // Since the active item has changed, the active menu trail may also be out + // of date. + drupal_static_reset('menu_set_active_trail'); + // @todo Refactor to use the Symfony Request object. + _current_path($path); +} + +/** + * Sets the active trail (path to the menu tree root) of the current page. + * + * Any trail set by this function will only be used for functionality that calls + * menu_get_active_trail(). Drupal core only uses trails set here for + * breadcrumbs and the page title and not for menu trees or page content. + * Additionally, breadcrumbs set by drupal_set_breadcrumb() will override any + * trail set here. + * + * To affect the trail used by menu trees, use menu_tree_set_path(). To affect + * the page content, use menu_set_active_item() instead. + * + * @param $new_trail + * Menu trail to set; the value is saved in a static variable and can be + * retrieved by menu_get_active_trail(). The format of this array should be + * the same as the return value of menu_get_active_trail(). + * + * @return + * The active trail. See menu_get_active_trail() for details. + */ +function menu_set_active_trail($new_trail = NULL) { + $trail = &drupal_static(__FUNCTION__); + + if (isset($new_trail)) { + $trail = $new_trail; + } + elseif (!isset($trail)) { + $trail = array(); + $trail[] = array( + 'title' => t('Home'), + 'href' => '', + 'link_path' => '', + 'localized_options' => array(), + 'type' => 0, + ); + + // Try to retrieve a menu link corresponding to the current path. If more + // than one exists, the link from the most preferred menu is returned. + $preferred_link = menu_link_get_preferred(); + $current_item = menu_get_item(); + + // There is a link for the current path. + if ($preferred_link) { + // Pass TRUE for $only_active_trail to make menu_tree_page_data() build + // a stripped down menu tree containing the active trail only, in case + // the given menu has not been built in this request yet. + $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE); + list($key, $curr) = each($tree); + } + // There is no link for the current path. + else { + $preferred_link = $current_item; + $curr = FALSE; + } + + while ($curr) { + $link = $curr['link']; + if ($link['in_active_trail']) { + // Add the link to the trail, unless it links to its parent. + if (!($link['type'] & MENU_LINKS_TO_PARENT)) { + // The menu tree for the active trail may contain additional links + // that have not been translated yet, since they contain dynamic + // argument placeholders (%). Such links are not contained in regular + // menu trees, and have only been loaded for the additional + // translation that happens here, so as to be able to display them in + // the breadcumb for the current page. + // @see _menu_tree_check_access() + // @see _menu_link_translate() + if (strpos($link['href'], '%') !== FALSE) { + _menu_link_translate($link, TRUE); + } + if ($link['access']) { + $trail[] = $link; + } + } + $tree = $curr['below'] ? $curr['below'] : array(); + } + list($key, $curr) = each($tree); + } + // Make sure the current page is in the trail to build the page title, by + // appending either the preferred link or the menu router item for the + // current page. Exclude it if we are on the front page. + $last = end($trail); + if ($preferred_link && $last['href'] != $preferred_link['href'] && !drupal_is_front_page()) { + $trail[] = $preferred_link; + } + } + return $trail; +} + +/** + * @addtogroup menu + * @{ + */ /** * Prepares variables for single local task link templates. diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml index 2f26a38..0696442 100644 --- a/core/modules/block/block.routing.yml +++ b/core/modules/block/block.routing.yml @@ -20,8 +20,8 @@ entity.block.delete_form: entity.block.edit_form: path: '/admin/structure/block/manage/{block}' defaults: - _entity_form: 'block.default' _title: 'Configure block' + _entity_form: 'block.edit' requirements: _entity_access: 'block.update' diff --git a/core/modules/block/lib/Drupal/block/BlockFormController.php b/core/modules/block/lib/Drupal/block/BlockFormController.php new file mode 100644 index 0000000..b256e08 --- /dev/null +++ b/core/modules/block/lib/Drupal/block/BlockFormController.php @@ -0,0 +1,407 @@ +storageController = $entity_manager->getStorageController('block'); + $this->entityQueryFactory = $entity_query_factory; + $this->languageManager = $language_manager; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('entity.query'), + $container->get('language_manager'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, array &$form_state) { + $entity = $this->entity; + $form['#tree'] = TRUE; + $form['id'] = array( + '#type' => 'value', + '#value' => $entity->id(), + ); + $form['settings'] = $entity->getPlugin()->buildConfigurationForm(array(), $form_state); + + // If creating a new block, calculate a safe default machine name. + $form['machine_name'] = array( + '#type' => 'machine_name', + '#maxlength' => 64, + '#description' => $this->t('A unique name for this block instance. Must be alpha-numeric and underscore separated.'), + '#default_value' => !$entity->isNew() ? $entity->id() : $this->getUniqueMachineName($entity), + '#machine_name' => array( + 'exists' => 'block_load', + 'replace_pattern' => '[^a-z0-9_.]+', + 'source' => array('settings', 'label'), + ), + '#required' => TRUE, + '#disabled' => !$entity->isNew(), + ); + + // Visibility settings. + $form['visibility'] = array( + '#type' => 'vertical_tabs', + '#title' => $this->t('Visibility settings'), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'block') . '/block.js'), + ), + '#tree' => TRUE, + '#weight' => 10, + '#parents' => array('visibility'), + ); + + // Per-path visibility. + $form['visibility']['path'] = array( + '#type' => 'details', + '#title' => $this->t('Pages'), + '#collapsed' => TRUE, + '#group' => 'visibility', + '#weight' => 0, + ); + + // @todo remove this access check and inject it in some other way. In fact + // this entire visibility settings section probably needs a separate user + // interface in the near future. + $visibility = $entity->get('visibility'); + $access = $this->currentUser()->hasPermission('use PHP for settings'); + if (!empty($visibility['path']['visibility']) && $visibility['path']['visibility'] == BLOCK_VISIBILITY_PHP && !$access) { + $form['visibility']['path']['visibility'] = array( + '#type' => 'value', + '#value' => BLOCK_VISIBILITY_PHP, + ); + $form['visibility']['path']['pages'] = array( + '#type' => 'value', + '#value' => !empty($visibility['path']['pages']) ? $visibility['path']['pages'] : '', + ); + } + else { + $options = array( + BLOCK_VISIBILITY_NOTLISTED => $this->t('All pages except those listed'), + BLOCK_VISIBILITY_LISTED => $this->t('Only the listed pages'), + ); + $description = $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %user for the current user's page and %user-wildcard for every user page. %front is the front page.", array('%user' => 'user', '%user-wildcard' => 'user/*', '%front' => '')); + + if ($this->moduleHandler->moduleExists('php') && $access) { + $options += array(BLOCK_VISIBILITY_PHP => $this->t('Pages on which this PHP code returns TRUE (experts only)')); + $title = $this->t('Pages or PHP code'); + $description .= ' ' . $this->t('If the PHP option is chosen, enter PHP code between %php. Note that executing incorrect PHP code can break your Drupal site.', array('%php' => '')); + } + else { + $title = $this->t('Pages'); + } + $form['visibility']['path']['visibility'] = array( + '#type' => 'radios', + '#title' => $this->t('Show block on specific pages'), + '#options' => $options, + '#default_value' => !empty($visibility['path']['visibility']) ? $visibility['path']['visibility'] : BLOCK_VISIBILITY_NOTLISTED, + ); + $form['visibility']['path']['pages'] = array( + '#type' => 'textarea', + '#title' => '' . $title . '', + '#default_value' => !empty($visibility['path']['pages']) ? $visibility['path']['pages'] : '', + '#description' => $description, + ); + } + + // Configure the block visibility per language. + if ($this->moduleHandler->moduleExists('language') && $this->languageManager->isMultilingual()) { + $configurable_language_types = language_types_get_configurable(); + + // Fetch languages. + $languages = language_list(Language::STATE_ALL); + $langcodes_options = array(); + foreach ($languages as $language) { + // @todo $language->name is not wrapped with t(), it should be replaced + // by CMI translation implementation. + $langcodes_options[$language->id] = $language->name; + } + $form['visibility']['language'] = array( + '#type' => 'details', + '#title' => $this->t('Languages'), + '#collapsed' => TRUE, + '#group' => 'visibility', + '#weight' => 5, + ); + // If there are multiple configurable language types, let the user pick + // which one should be applied to this visibility setting. This way users + // can limit blocks by interface language or content language for example. + $language_types = language_types_info(); + $language_type_options = array(); + foreach ($configurable_language_types as $type_key) { + $language_type_options[$type_key] = $language_types[$type_key]['name']; + } + $form['visibility']['language']['language_type'] = array( + '#type' => 'radios', + '#title' => $this->t('Language type'), + '#options' => $language_type_options, + '#default_value' => !empty($visibility['language']['language_type']) ? $visibility['language']['language_type'] : $configurable_language_types[0], + '#access' => count($language_type_options) > 1, + ); + $form['visibility']['language']['langcodes'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Show this block only for specific languages'), + '#default_value' => !empty($visibility['language']['langcodes']) ? $visibility['language']['langcodes'] : array(), + '#options' => $langcodes_options, + '#description' => $this->t('Show this block only for the selected language(s). If you select no languages, the block will be visible in all languages.'), + ); + } + + // Per-role visibility. + $role_options = array_map('check_plain', user_role_names()); + $form['visibility']['role'] = array( + '#type' => 'details', + '#title' => $this->t('Roles'), + '#collapsed' => TRUE, + '#group' => 'visibility', + '#weight' => 10, + ); + $form['visibility']['role']['roles'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Show block for specific roles'), + '#default_value' => !empty($visibility['role']['roles']) ? $visibility['role']['roles'] : array(), + '#options' => $role_options, + '#description' => $this->t('Show this block only for the selected role(s). If you select no roles, the block will be visible to all users.'), + ); + + // Theme settings. + if ($theme = $entity->get('theme')) { + $form['theme'] = array( + '#type' => 'value', + '#value' => $entity->get('theme'), + ); + } + else { + $theme_options = array(); + foreach (list_themes() as $theme_name => $theme_info) { + if (!empty($theme_info->status)) { + $theme_options[$theme_name] = $theme_info->info['name']; + } + } + $theme = $this->configFactory->get('system.theme')->get('default'); + $form['theme'] = array( + '#type' => 'select', + '#options' => $theme_options, + '#title' => t('Theme'), + '#default_value' => $theme, + '#ajax' => array( + 'callback' => array($this, 'themeSwitch'), + 'wrapper' => 'edit-block-region-wrapper', + ), + ); + } + // Region settings. + $form['region'] = array( + '#type' => 'select', + '#title' => $this->t('Region'), + '#description' => $this->t('Select the region where this block should be displayed.'), + '#default_value' => $entity->get('region'), + '#empty_value' => BLOCK_REGION_NONE, + '#options' => system_region_list($theme, REGIONS_VISIBLE), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + return $form; + } + + /** + * Handles switching the available regions based on the selected theme. + */ + public function themeSwitch($form, &$form_state) { + $form['region']['#options'] = system_region_list($form_state['values']['theme'], REGIONS_VISIBLE); + return $form['region']; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, array &$form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save block'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + $entity = $this->entity; + if ($entity->isNew()) { + form_set_value($form['id'], $form_state['values']['theme'] . '.' . $form_state['values']['machine_name'], $form_state); + } + if (!empty($form['machine_name']['#disabled'])) { + $config_id = explode('.', $form_state['values']['machine_name']); + $form_state['values']['machine_name'] = array_pop($config_id); + } + $form_state['values']['visibility']['role']['roles'] = array_filter($form_state['values']['visibility']['role']['roles']); + // The Block Entity form puts all block plugin form elements in the + // settings form element, so just pass that to the block for validation. + $settings = array( + 'values' => &$form_state['values']['settings'] + ); + // Call the plugin validate handler. + $entity->getPlugin()->validateConfigurationForm($form, $settings); + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + parent::submit($form, $form_state); + + $entity = $this->entity; + // The Block Entity form puts all block plugin form elements in the + // settings form element, so just pass that to the block for submission. + $settings = array( + 'values' => &$form_state['values']['settings'] + ); + // Call the plugin submit handler. + $entity->getPlugin()->submitConfigurationForm($form, $settings); + + // Save the settings of the plugin. + $entity->save(); + + if ($this->operation == 'add') { + $theme = $this->entity->get('theme'); + $regions = system_region_list($theme); + $region = $this->entity->get('region'); + $themes = list_themes(); + if (isset($regions[$region])) { + drupal_set_message($this->t("Block placed in @theme's %region region.", array( + '@theme' => $themes[$theme]->info['name'], + '%region' => $regions[$region], + ))); + } + else { + drupal_set_message($this->t('Block added to @theme but not placed in a region.', array( + '@theme' => $themes[$theme]->info['name'], + ))); + } + } + else { + drupal_set_message($this->t('The block configuration has been saved.')); + } + Cache::invalidateTags(array('content' => TRUE)); + $form_state['redirect'] = array('admin/structure/block/list/' . $form_state['values']['theme'], array( + 'query' => array('block-placement' => drupal_html_class($this->entity->id())), + )); + } + + /** + * {@inheritdoc} + */ + public function delete(array $form, array &$form_state) { + parent::delete($form, $form_state); + $form_state['redirect'] = 'admin/structure/block/manage/' . $this->entity->id() . '/delete'; + } + + /** + * Generates a unique machine name for a block. + * + * @param \Drupal\block\BlockInterface $block + * The block entity. + * + * @return string + * Returns the unique name. + */ + public function getUniqueMachineName(BlockInterface $block) { + $suggestion = $block->getPlugin()->getMachineNameSuggestion(); + + // Get all the blocks which starts with the suggested machine name. + $query = $this->entityQueryFactory->get('block'); + $query->condition('id', $suggestion, 'CONTAINS'); + $block_ids = $query->execute(); + + $block_ids = array_map(function ($block_id) { + $parts = explode('.', $block_id); + return end($parts); + }, $block_ids); + + // Iterate through potential IDs until we get a new one. E.g. + // 'plugin', 'plugin_2', 'plugin_3'... + $count = 1; + $machine_default = $suggestion; + while (in_array($machine_default, $block_ids)) { + $machine_default = $suggestion . '_' . ++$count; + } + return $machine_default; + } + +} diff --git a/core/modules/block/lib/Drupal/block/Entity/Block.php b/core/modules/block/lib/Drupal/block/Entity/Block.php new file mode 100644 index 0000000..c5c41ab --- /dev/null +++ b/core/modules/block/lib/Drupal/block/Entity/Block.php @@ -0,0 +1,195 @@ +pluginBag = new BlockPluginBag(\Drupal::service('plugin.manager.block'), array($this->plugin), $this->get('settings'), $this->id()); + } + + /** + * {@inheritdoc} + */ + public function getPlugin() { + return $this->pluginBag->get($this->plugin); + } + + /** + * Overrides \Drupal\Core\Entity\Entity::uri(); + */ + public function uri() { + return array( + 'path' => 'admin/structure/block/manage/' . $this->id(), + 'options' => array( + 'entity_type' => $this->entityType, + 'entity' => $this, + ), + ); + } + /** + * Overrides \Drupal\Core\Entity\Entity::label(); + */ + public function label($langcode = NULL) { + $settings = $this->get('settings'); + return $settings['label']; + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::get(); + */ + public function get($property_name, $langcode = NULL) { + // The theme is stored in the entity ID. + $value = parent::get($property_name, $langcode); + if ($property_name == 'theme' && !$value) { + list($value) = explode('.', $this->id()); + } + return $value; + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::getExportProperties(); + */ + public function getExportProperties() { + $properties = parent::getExportProperties(); + $names = array( + 'region', + 'weight', + 'plugin', + 'settings', + 'visibility', + ); + foreach ($names as $name) { + $properties[$name] = $this->get($name); + } + return $properties; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageControllerInterface $storage_controller) { + $this->set('settings', $this->getPlugin()->getConfiguration()); + } + + /** + * Sorts active blocks by weight; sorts inactive blocks by name. + */ + public static function sort($a, $b) { + // Separate enabled from disabled. + $status = $b->get('status') - $a->get('status'); + if ($status) { + return $status; + } + // Sort by weight, unless disabled. + if ($a->get('region') != BLOCK_REGION_NONE) { + $weight = $a->get('weight') - $b->get('weight'); + if ($weight) { + return $weight; + } + } + // Sort by label. + return strcmp($a->label(), $b->label()); + } + +} diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php new file mode 100644 index 0000000..72c778f --- /dev/null +++ b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php @@ -0,0 +1,367 @@ + 'Block functionality', + 'description' => 'Tests basic block functionality.', + 'group' => 'Block', + ); + } + + /** + * Tests block visibility. + */ + function testBlockVisibility() { + $block_name = 'system_powered_by_block'; + // Create a random title for the block. + $title = $this->randomName(8); + // Enable a standard block. + $default_theme = \Drupal::config('system.theme')->get('default'); + $edit = array( + 'machine_name' => strtolower($this->randomName(8)), + 'region' => 'sidebar_first', + 'settings[label]' => $title, + ); + // Set the block to be hidden on any user path, and to be shown only to + // authenticated users. + $edit['visibility[path][pages]'] = 'user*'; + $edit['visibility[role][roles][' . DRUPAL_AUTHENTICATED_RID . ']'] = TRUE; + $this->drupalPostForm('admin/structure/block/add/' . $block_name . '/' . $default_theme, $edit, t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Left sidebar')), 'Block was saved'); + + $this->drupalGet(''); + $this->assertText($title, 'Block was displayed on the front page.'); + + $this->drupalGet('user'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules.'); + + $this->drupalGet('USER/' . $this->adminUser->id()); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules regardless of path case.'); + + // Confirm that the block is not displayed to anonymous users. + $this->drupalLogout(); + $this->drupalGet(''); + $this->assertNoText($title, 'Block was not displayed to anonymous users.'); + + // Confirm that an empty block is not displayed. + $this->assertNoText('Powered by Drupal', 'Empty block not displayed.'); + $this->assertNoRaw('sidebar-first', 'Empty sidebar-first region is not displayed.'); + } + + /** + * Test block visibility when using "pages" restriction but leaving + * "pages" textarea empty + */ + function testBlockVisibilityListedEmpty() { + $block_name = 'system_powered_by_block'; + // Create a random title for the block. + $title = $this->randomName(8); + // Enable a standard block. + $default_theme = \Drupal::config('system.theme')->get('default'); + $edit = array( + 'machine_name' => strtolower($this->randomName(8)), + 'region' => 'sidebar_first', + 'settings[label]' => $title, + 'visibility[path][visibility]' => BLOCK_VISIBILITY_LISTED, + ); + // Set the block to be hidden on any user path, and to be shown only to + // authenticated users. + $this->drupalPostForm('admin/structure/block/add/' . $block_name . '/' . $default_theme, $edit, t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Left sidebar')), 'Block was saved'); + + $this->drupalGet('user'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules.'); + + $this->drupalGet('USER'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules regardless of path case.'); + + // Confirm that the block is not displayed to anonymous users. + $this->drupalLogout(); + $this->drupalGet(''); + $this->assertNoText($title, 'Block was not displayed to anonymous users on the front page.'); + } + + /** + * Test configuring and moving a module-define block to specific regions. + */ + function testBlock() { + // Select the 'Powered by Drupal' block to be configured and moved. + $block = array(); + $block['id'] = 'system_powered_by_block'; + $block['settings[label]'] = $this->randomName(8); + $block['machine_name'] = strtolower($this->randomName(8)); + $block['theme'] = \Drupal::config('system.theme')->get('default'); + $block['region'] = 'header'; + + // Set block title to confirm that interface works and override any custom titles. + $this->drupalPostForm('admin/structure/block/add/' . $block['id'] . '/' . $block['theme'], array('settings[label]' => $block['settings[label]'], 'machine_name' => $block['machine_name'], 'region' => $block['region']), t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Header')), 'Block title set.'); + // Check to see if the block was created by checking its configuration. + $instance = entity_load('block', $block['theme'] . '.' . $block['machine_name']); + + $this->assertEqual($instance->label(), $block['settings[label]'], 'Stored block title found.'); + + // Check whether the block can be moved to all available regions. + foreach ($this->regions as $region) { + $this->moveBlockToRegion($block, $region); + } + + // Set the block to the disabled region. + $edit = array(); + $edit['blocks[' . $block['theme'] . '.' . $block['machine_name'] . '][region]'] = -1; + $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks')); + + // Confirm that the block is now listed as disabled. + $this->assertText(t('The block settings have been updated.'), 'Block successfully move to disabled region.'); + + // Confirm that the block instance title and markup are not displayed. + $this->drupalGet('node'); + $this->assertNoText(t($block['settings[label]'])); + // Check for
    if the machine name + // is my_block_instance_name. + $xpath = $this->buildXPathQuery('//div[@id=:id]/*', array(':id' => 'block-' . strtr(strtolower($block['machine_name']), '-', '_'))); + $this->assertNoFieldByXPath($xpath, FALSE, 'Block found in no regions.'); + + // Test deleting the block from the edit form. + $this->drupalGet('admin/structure/block/manage/' . $block['theme'] . '.' . $block['machine_name']); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertRaw(t('Are you sure you want to delete the block %name?', array('%name' => $block['settings[label]']))); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertRaw(t('The block %name has been removed.', array('%name' => $block['settings[label]']))); + } + + /** + * Tests that the block form has a theme selector when not passed via the URL. + */ + public function testBlockThemeSelector() { + // Enable all themes. + theme_enable(array('bartik', 'seven')); + $theme_settings = $this->container->get('config.factory')->get('system.theme'); + $themes = list_themes(); + foreach (array('bartik', 'stark', 'seven') as $theme) { + // Select the 'Powered by Drupal' block to be placed. + $block = array(); + $block['machine_name'] = strtolower($this->randomName(8)); + $block['theme'] = $theme; + $block['region'] = 'content'; + $this->drupalPostForm('admin/structure/block/add/system_powered_by_block', $block, t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => $themes[$theme]->info['name'], '%region' => 'Content'))); + $this->assertUrl('admin/structure/block/list/' . $theme . '?block-placement=' . drupal_html_class($theme . ':' . $block['machine_name'])); + + // Set the default theme and ensure the block is placed. + $theme_settings->set('default', $theme)->save(); + $this->drupalGet(''); + $elements = $this->xpath('//div[@id = :id]', array(':id' => drupal_html_id('block-' . $block['machine_name']))); + $this->assertTrue(!empty($elements), 'The block was found.'); + } + } + + /** + * Test block title display settings. + */ + function testHideBlockTitle() { + $block_name = 'system_powered_by_block'; + // Create a random title for the block. + $title = $this->randomName(8); + $machine_name = strtolower($this->randomName(8)); + // Enable a standard block. + $default_theme = variable_get('theme_default', 'stark'); + $edit = array( + 'machine_name' => $machine_name, + 'region' => 'sidebar_first', + 'settings[label]' => $title, + ); + $this->drupalPostForm('admin/structure/block/add/' . $block_name . '/' . $default_theme, $edit, t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Left sidebar')), 'Block was saved'); + + $this->drupalGet('user'); + $this->assertText($title, 'Block title was displayed by default.'); + + $edit = array( + 'settings[label_display]' => FALSE, + ); + $this->drupalPostForm('admin/structure/block/manage/' . $default_theme . '.' . $machine_name, $edit, t('Save block')); + $this->assertText('The block configuration has been saved.', 'Block was saved'); + + $this->drupalGet('user'); + $this->assertNoText($title, 'Block title was not displayed when hidden.'); + } + + /** + * Moves a block to a given region via the UI and confirms the result. + * + * @param array $block + * An array of information about the block, including the following keys: + * - module: The module providing the block. + * - title: The title of the block. + * - delta: The block's delta key. + * @param string $region + * The machine name of the theme region to move the block to, for example + * 'header' or 'sidebar_first'. + */ + function moveBlockToRegion(array $block, $region) { + // Set the created block to a specific region. + $block += array('theme' => \Drupal::config('system.theme')->get('default')); + $edit = array(); + $edit['blocks[' . $block['theme'] . '.' . $block['machine_name'] . '][region]'] = $region; + $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks')); + + // Confirm that the block was moved to the proper region. + $this->assertText(t('The block settings have been updated.'), format_string('Block successfully moved to %region_name region.', array( '%region_name' => $region))); + + // Confirm that the block is being displayed. + $this->drupalGet(''); + $this->assertText(t($block['settings[label]']), 'Block successfully being displayed on the page.'); + + // Confirm that the custom block was found at the proper region. + $xpath = $this->buildXPathQuery('//div[@class=:region-class]//div[@id=:block-id]/*', array( + ':region-class' => 'region region-' . drupal_html_class($region), + ':block-id' => 'block-' . strtr(strtolower($block['machine_name']), '-', '_'), + )); + $this->assertFieldByXPath($xpath, NULL, t('Block found in %region_name region.', array('%region_name' => drupal_html_class($region)))); + } + + /** + * Test _block_rehash(). + */ + function testBlockRehash() { + module_enable(array('block_test')); + $this->assertTrue(module_exists('block_test'), 'Test block module enabled.'); + + // Clear the block cache to load the block_test module's block definitions. + $this->container->get('plugin.manager.block')->clearCachedDefinitions(); + + // Add a test block. + $block = array(); + $block['id'] = 'test_cache'; + $block['machine_name'] = strtolower($this->randomName(8)); + $block['theme'] = \Drupal::config('system.theme')->get('default'); + $block['region'] = 'header'; + $block = $this->drupalPlaceBlock('test_cache', array('region' => 'header')); + + // Our test block's caching should default to DRUPAL_CACHE_PER_ROLE. + $settings = $block->get('settings'); + $this->assertEqual($settings['cache'], DRUPAL_CACHE_PER_ROLE, 'Test block cache mode defaults to DRUPAL_CACHE_PER_ROLE.'); + + // Disable caching for this block. + $block->getPlugin()->setConfigurationValue('cache', DRUPAL_NO_CACHE); + $block->save(); + // Flushing all caches should call _block_rehash(). + $this->resetAll(); + // Verify that block is updated with the new caching mode. + $block = entity_load('block', $block->id()); + $settings = $block->get('settings'); + $this->assertEqual($settings['cache'], DRUPAL_NO_CACHE, "Test block's database entry updated to DRUPAL_NO_CACHE."); + } + + /** + * Tests blocks belonging to disabled modules. + */ + function testBlockModuleDisable() { + module_enable(array('block_test')); + $this->assertTrue(module_exists('block_test'), 'Test block module enabled.'); + + // Clear the block cache to load the block_test module's block definitions. + $manager = $this->container->get('plugin.manager.block'); + $manager->clearCachedDefinitions(); + + // Add test blocks in different regions and confirm they are displayed. + $blocks = array(); + $regions = array('sidebar_first', 'content', 'footer'); + foreach ($regions as $region) { + $blocks[$region] = $this->drupalPlaceBlock('test_cache', array('region' => $region)); + } + $this->drupalGet(''); + foreach ($regions as $region) { + $this->assertText($blocks[$region]->label()); + } + + // Disable the block test module and refresh the definitions cache. + module_disable(array('block_test'), FALSE); + $this->assertFalse(module_exists('block_test'), 'Test block module disabled.'); + $manager->clearCachedDefinitions(); + + // Ensure that the block administration page still functions as expected. + $this->drupalGet('admin/structure/block'); + $this->assertResponse(200); + // A 200 response is possible with a fatal error, so check the title too. + $this->assertTitle(t('Block layout') . ' | Drupal'); + + // Ensure that the disabled module's block instance is not listed. + foreach ($regions as $region) { + $this->assertNoText($blocks[$region]->label()); + } + + // Ensure that the disabled module's block plugin is no longer available. + $this->drupalGet('admin/structure/block/list/' . \Drupal::config('system.theme')->get('default')); + $this->assertNoText(t('Test block caching')); + + // Confirm that the block is no longer displayed on the front page. + $this->drupalGet(''); + $this->assertResponse(200); + foreach ($regions as $region) { + $this->assertNoText($blocks[$region]->label()); + } + + // Confirm that a different block instance can still be enabled by + // submitting the block library form. + // Emulate a POST submission rather than using drupalPlaceBlock() to ensure + // that the form still functions as expected. + $edit = array( + 'settings[label]' => $this->randomName(8), + 'machine_name' => strtolower($this->randomName(8)), + 'region' => 'sidebar_first', + ); + $this->drupalPostForm('admin/structure/block/add/system_powered_by_block/stark', $edit, t('Save block')); + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Left sidebar'))); + $this->assertText($edit['settings[label]']); + + // Update the weight of a block. + $edit = array('blocks[stark.' . $edit['machine_name'] . '][weight]' => -1); + $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks')); + $this->assertText(t('The block settings have been updated.')); + + // Re-enable the module and refresh the definitions cache. + module_enable(array('block_test'), FALSE); + $this->assertTrue(module_exists('block_test'), 'Test block module re-enabled.'); + $manager->clearCachedDefinitions(); + + // Reload the admin page and confirm the block can again be configured. + $this->drupalGet('admin/structure/block'); + foreach ($regions as $region) { + $this->assertLinkByHref(url('admin/structure/block/manage/' . $blocks[$region]->id())); + } + + // Confirm that the blocks are again displayed on the front page in the + // correct regions. + $this->drupalGet(''); + foreach ($regions as $region) { + // @todo Use a proper method for this. + $name_pieces = explode('.', $blocks[$region]->id()); + $machine_name = array_pop($name_pieces); + $xpath = $this->buildXPathQuery('//div[@class=:region-class]//div[@id=:block-id]/*', array( + ':region-class' => 'region region-' . drupal_html_class($region), + ':block-id' => 'block-' . strtr(strtolower($machine_name), '-', '_'), + )); + $this->assertFieldByXPath($xpath, NULL, format_string('Block %name found in the %region region.', array( + '%name' => $blocks[$region]->label(), + '%region' => $region, + ))); + } + } + +} diff --git a/core/modules/block/src/Controller/BlockAddController.php b/core/modules/block/src/Controller/BlockAddController.php index dacfde3..63b13b3 100644 --- a/core/modules/block/src/Controller/BlockAddController.php +++ b/core/modules/block/src/Controller/BlockAddController.php @@ -30,7 +30,6 @@ public function blockAddConfigureForm($plugin_id, $theme) { // Create a block entity. $entity = $this->entityManager()->getStorage('block')->create(array('plugin' => $plugin_id, 'theme' => $theme)); - return $this->entityFormBuilder()->getForm($entity); + return $this->entityManager()->getForm($entity, 'add'); } - } diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuBlockTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuBlockTest.php new file mode 100644 index 0000000..c2d5ca6 --- /dev/null +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuBlockTest.php @@ -0,0 +1,61 @@ + 'Menu block', + 'description' => 'Tests block integration of menus.', + 'group' => 'Menu' + ); + } + + /** + * Tests placing menu blocks. + */ + public function testMenuBlockPlacement() { + // Try editing without block permissions. + $this->drupalLogin($this->drupalCreateUser(array('administer menu'))); + $this->drupalGet('admin/structure/menu'); + $this->clickLink(t('Edit menu')); + $this->assertNoLink(t('Place block')); + + // Place a menu block with proper permissions. + $this->drupalLogin($this->drupalCreateUser(array('administer menu', 'administer blocks'))); + $this->drupalGet('admin/structure/menu'); + $this->clickLink(t('Edit menu')); + + // Store the URL used to edit the menu before placing the block. + $menu_edit_url = $this->getUrl(); + $this->clickLink(t('Place block')); + $this->drupalPostForm(NULL, array(), t('Save block')); + // Assert that the message acknowledged that no region was specified. + $this->assertRaw(t('Block added to @theme but not placed in a region.', array('@theme' => 'Stark'))); + $this->assertUrl($menu_edit_url, array(), 'After saving the block, the redirect is back to the menu edit page.'); + + $this->clickLink(t('Place block')); + $this->drupalPostForm(NULL, array('region' => 'content'), t('Save block')); + // Verify the message displayed when a valid region was chosen. + $this->assertRaw(t("Block placed in @theme's %region region.", array('@theme' => 'Stark', '%region' => 'Content'))); + } + +} diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module new file mode 100644 index 0000000..2a0dace --- /dev/null +++ b/core/modules/menu/menu.module @@ -0,0 +1,774 @@ +' . t('About') . ''; + $output .= '

    ' . t('The Menu module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. Each menu is rendered in a block that can be enabled and positioned through the Blocks administration page. You can view and manage menus on the Menus administration page. For more information, see the online handbook entry for the Menu module.', array('@blocks' => url('admin/structure/block'), '@menus' => url('admin/structure/menu'), '@menu' => 'http://drupal.org/documentation/modules/menu/')) . '

    '; + $output .= '

    ' . t('Uses') . '

    '; + $output .= '
    '; + $output .= '
    ' . t('Managing menus') . '
    '; + $output .= '
    ' . t('Users with the Administer menus and menu items permission can add, edit and delete custom menus on the Menus administration page. Custom menus can be special site menus, menus of external links, or any combination of internal and external links. You may create an unlimited number of additional menus, each of which will automatically have an associated block. By selecting list links, you can add, edit, or delete links for a given menu. The links listing page provides a drag-and-drop interface for controlling the order of links, and creating a hierarchy within the menu.', array('@menu' => url('admin/structure/menu'), '@add-menu' => url('admin/structure/menu/add'))) . '
    '; + $output .= '
    ' . t('Displaying menus') . '
    '; + $output .= '
    ' . t('After you have created a menu, you must enable and position the associated block on the Blocks administration page.', array('@blocks' => url('admin/structure/block'))) . '
    '; + $output .= '
    '; + return $output; + + case 'admin/structure/menu/add': + return '

    ' . t('You can enable the newly-created block for this menu on the Blocks administration page.', array('@blocks' => url('admin/structure/block'))) . '

    '; + + case 'admin/structure/menu/manage/%': + $output = ''; + $output .= '

    ' . t('On this page you can rename the menu and reorder or edit existing menu links. In addition, links are provided to add a new menu link, or place a block for this menu in any theme.') . '

    '; + return $output; + } + if ($path == 'admin/structure/menu' && module_exists('block')) { + return '

    ' . t('Each menu has a corresponding block that is managed on the Blocks administration page.', array('@blocks' => url('admin/structure/block'))) . '

    '; + } +} + +/** + * Implements hook_permission(). + */ +function menu_permission() { + return array( + 'administer menu' => array( + 'title' => t('Administer menus and menu items'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function menu_menu() { + $items['admin/structure/menu'] = array( + 'title' => 'Menus', + 'description' => 'Add new menus to your site, edit existing menus, and rename and reorganize menu links.', + 'route_name' => 'menu_overview_page', + ); + $items['admin/structure/menu/list'] = array( + 'title' => 'List menus', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/structure/menu/add'] = array( + 'title' => 'Add menu', + 'route_name' => 'menu_menu_add', + 'type' => MENU_LOCAL_ACTION, + ); + $items['admin/structure/menu/settings'] = array( + 'title' => 'Settings', + 'route_name' => 'menu_settings', + 'type' => MENU_LOCAL_TASK, + 'weight' => 100, + ); + $items['admin/structure/menu/manage/%menu'] = array( + 'title' => 'Edit menu', + 'route_name' => 'menu_menu_edit', + 'title callback' => 'entity_page_label', + 'title arguments' => array(4), + ); + $items['admin/structure/menu/manage/%menu/add'] = array( + 'title' => 'Add link', + 'route_name' => 'menu_link_add', + 'type' => MENU_LOCAL_ACTION, + ); + $items['admin/structure/menu/manage/%menu/edit'] = array( + 'title' => 'Edit menu', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + ); + $items['admin/structure/menu/manage/%menu/delete'] = array( + 'title' => 'Delete menu', + 'route_name' => 'menu_delete_menu', + ); + $items['admin/structure/menu/item/%menu_link/edit'] = array( + 'title' => 'Edit menu link', + 'route_name' => 'menu_link_edit', + ); + $items['admin/structure/menu/item/%menu_link/reset'] = array( + 'title' => 'Reset menu link', + 'route_name' => 'menu_link_reset', + ); + $items['admin/structure/menu/item/%menu_link/delete'] = array( + 'title' => 'Delete menu link', + 'route_name' => 'menu_link_delete', + ); + return $items; +} + +/** + * Implements hook_menu_local_tasks(). + */ +function menu_menu_local_tasks(&$data, $router_item, $root_path) { + if (Drupal::moduleHandler()->moduleExists('block') && $router_item['route_name'] == 'menu_menu_edit') { + // @todo Move to a LocalAction plugin when https://drupal.org/node/2045267 + // allows local actions to work with query strings. + $path = 'admin/structure/block/add/system_menu_block:' . end($router_item['original_map']); + $item = menu_get_item($path); + if ($item['access']) { + $item['localized_options']['query']['destination'] = Drupal::request()->attributes->get('_system_path'); + $item['localized_options']['attributes'] = array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => Json::encode(array( + 'width' => 700, + )), + ); + $data['actions'][$path] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + '#weight' => 100, + ); + } + } +} + +/** + * Implements hook_entity_info(). + */ +function menu_entity_info(&$entity_info) { + $entity_info['menu']['controllers']['list'] = 'Drupal\menu\MenuListController'; + $entity_info['menu']['uri_callback'] = 'menu_uri'; + $entity_info['menu']['controllers']['form'] = array( + 'add' => 'Drupal\menu\MenuFormController', + 'edit' => 'Drupal\menu\MenuFormController', + 'delete' => 'Drupal\menu\Form\MenuDeleteForm', + ); + + $entity_info['menu_link']['controllers']['form']['delete'] = 'Drupal\menu\Form\MenuLinkDeleteForm'; + $entity_info['menu_link']['controllers']['form']['reset'] = 'Drupal\menu\Form\MenuLinkResetForm'; +} + +/** + * Implements hook_entity_bundle_info(). + */ +function menu_entity_bundle_info() { + $bundles = array(); + $config_names = config_get_storage_names_with_prefix('system.menu.'); + foreach ($config_names as $config_name) { + $config = Drupal::config($config_name); + $bundles['menu_link'][$config->get('id')] = array( + 'label' => $config->get('label'), + ); + } + + return $bundles; +} + +/** + * Entity URI callback. + * + * @param \Drupal\system\Entity\Menu $menu + * A Menu entity. + */ +function menu_uri(Menu $menu) { + return array( + 'path' => 'admin/structure/menu/manage/' . $menu->id(), + ); +} + +/** + * Implements hook_theme(). + */ +function menu_theme() { + return array( + 'menu_overview_form' => array( + 'file' => 'menu.admin.inc', + 'render element' => 'form', + ), + ); +} + +/** + * Implements hook_enable(). + * + * Add a link for each custom menu. + */ +function menu_enable() { + Drupal::service('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); + + $base_link = entity_create('menu_link', array( + 'menu_name' => $system_link->menu_name, + 'router_path' => 'admin/structure/menu/manage/%menu', + 'module' => 'menu', + )); + + $menus = entity_load_multiple('menu'); + foreach ($menus as $menu) { + $link = $base_link->createDuplicate(); + $link->plid = $system_link->id(); + $link->link_title = $menu->label(); + $link->link_path = 'admin/structure/menu/manage/' . $menu->id(); + + $query = Drupal::entityQuery('menu_link') + ->condition('link_path', $link->link_path) + ->condition('plid', $link->plid); + $result = $query->execute(); + + if (empty($result)) { + $link->save(); + } + } + menu_cache_clear_all(); +} + +/** + * Load the data for a single custom menu. + * + * @param $menu_name + * The unique name of a custom menu to load. + * @return + * Array defining the custom menu, or NULL if the menu doesn't exist. + */ +function menu_load($menu_name) { + return entity_load('menu', $menu_name); +} + +/** + * Implements hook_menu_insert() + */ +function menu_menu_insert(Menu $menu) { + menu_cache_clear_all(); + // Invalidate the block cache to update menu-based derivatives. + if (module_exists('block')) { + Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } + // Make sure the menu is present in the active menus variable so that its + // items may appear in the menu active trail. + // See menu_set_active_menu_names(). + $config = Drupal::config('system.menu'); + + $active_menus = $config->get('active_menus_default') ?: array_keys(menu_get_menus()); + if (!in_array($menu->id(), $active_menus)) { + $active_menus[] = $menu->id(); + $config + ->set('active_menus_default', $active_menus) + ->save(); + } +} + +/** + * Implements hook_menu_update(). + */ +function menu_menu_update(Menu $menu) { + menu_cache_clear_all(); + // Invalidate the block cache to update menu-based derivatives. + if (module_exists('block')) { + Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } +} + +/** + * Implements hook_menu_predelete(). + */ +function menu_menu_predelete(Menu $menu) { + // Delete all links from the menu. + menu_delete_links($menu->id()); + + // Remove menu from active menus variable. + $active_menus = variable_get('menu_default_active_menus', array_keys(menu_get_menus())); + foreach ($active_menus as $i => $menu_name) { + if ($menu->id() == $menu_name) { + unset($active_menus[$i]); + variable_set('menu_default_active_menus', $active_menus); + } + } +} + +/** + * Implements hook_menu_delete(). + */ +function menu_menu_delete(Menu $menu) { + menu_cache_clear_all(); + + // Invalidate the block cache to update menu-based derivatives. + if (module_exists('block')) { + Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } +} + +/** + * Returns a list of menu links that are valid possible parents for the given + * menu link. + * + * @param array $menus + * An array of menu names and titles, such as from menu_get_menus(). + * @param \Drupal\menu_link\Entity\MenuLink $menu_link + * The menu link for which to generate a list of parents. + * If $menu_link->id() == 0 then the complete tree is returned. + * @param string $type + * The node type for which to generate a list of parents. + * If $item itself is a node type then $type is ignored. + * + * @return array + * An array of menu link titles keyed by a string containing the menu name and + * mlid. The list excludes the given item and its children. + * + * @todo This has to be turned into a #process form element callback. The + * 'override_parent_selector' variable is entirely superfluous. + */ +function menu_parent_options(array $menus, MenuLink $menu_link = NULL, $type = NULL) { + // The menu_links table can be practically any size and we need a way to + // allow contrib modules to provide more scalable pattern choosers. + // hook_form_alter is too late in itself because all the possible parents are + // retrieved here, unless override_parent_selector is set to TRUE. + if (Drupal::config('menu.settings')->get('override_parent_selector')) { + return array(); + } + + if (!$menu_link) { + $menu_link = entity_create('menu_link', array('mlid' => 0)); + } + + $available_menus = array(); + if (!$type) { + // If no node type is set, use all menus given to this function. + $available_menus = $menus; + } + else { + // If a node type is set, use all available menus for this type. + $type_menus = variable_get('menu_options_' . $type, array('main' => 'main')); + foreach ($type_menus as $menu) { + $available_menus[$menu] = $menu; + } + } + + return _menu_get_options($menus, $available_menus, $menu_link); +} + +/** + * Helper function to get the items of the given menu. + */ +function _menu_get_options($menus, $available_menus, $item) { + // If the item has children, there is an added limit to the depth of valid parents. + if (isset($item['parent_depth_limit'])) { + $limit = $item['parent_depth_limit']; + } + else { + $limit = _menu_parent_depth_limit($item); + } + + $options = array(); + foreach ($menus as $menu_name => $title) { + if (isset($available_menus[$menu_name])) { + $tree = menu_tree_all_data($menu_name, NULL); + $options[$menu_name . ':0'] = '<' . $title . '>'; + _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit); + } + } + return $options; +} + +/** + * Recursive helper function for menu_parent_options(). + */ +function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, $depth_limit) { + foreach ($tree as $data) { + if ($data['link']['depth'] > $depth_limit) { + // Don't iterate through any links on this level. + break; + } + if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) { + $title = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, FALSE); + if ($data['link']['hidden']) { + $title .= ' (' . t('disabled') . ')'; + } + $options[$menu_name . ':' . $data['link']['mlid']] = $title; + if ($data['below']) { + _menu_parents_recurse($data['below'], $menu_name, $indent . '--', $options, $exclude, $depth_limit); + } + } + } +} + +/** + * Implements hook_block_view_BASE_BLOCK_ID_alter() for 'system_menu_block'. + */ +function menu_block_view_system_menu_block_alter(array &$build, BlockPluginInterface $block) { + // Add contextual links for system menu blocks. + $menus = menu_list_system_menus(); + // @todo Clean up when http://drupal.org/node/1874498 lands. + list(, $menu_name) = explode(':', $block->getPluginId()); + if (isset($menus[$menu_name]) && isset($build['content'])) { + foreach (element_children($build['content']) as $key) { + $build['content']['#contextual_links']['menu'] = array('admin/structure/menu/manage', array($build['content'][$key]['#original_link']['menu_name'])); + } + } +} + +/** + * Implements hook_node_insert(). + */ +function menu_node_insert(EntityInterface $node) { + menu_node_save($node); +} + +/** + * Implements hook_node_update(). + */ +function menu_node_update(EntityInterface $node) { + menu_node_save($node); +} + +/** + * Helper for hook_node_insert() and hook_node_update(). + */ +function menu_node_save(EntityInterface $node) { + if (isset($node->menu)) { + $link = &$node->menu; + if (empty($link['enabled'])) { + if (!$link->isNew()) { + menu_link_delete($link['mlid']); + } + } + elseif (trim($link['link_title'])) { + $link['link_title'] = trim($link['link_title']); + $link['link_path'] = 'node/' . $node->id(); + if (trim($link['description'])) { + $link['options']['attributes']['title'] = trim($link['description']); + } + else { + // If the description field was left empty, remove the title attribute + // from the menu link. + unset($link['options']['attributes']['title']); + } + if (!menu_link_save($link)) { + drupal_set_message(t('There was an error saving the menu link.'), 'error'); + } + } + } +} + +/** + * Implements hook_node_predelete(). + */ +function menu_node_predelete(EntityInterface $node) { + // Delete all menu module links that point to this node. + $query = Drupal::entityQuery('menu_link') + ->condition('link_path', 'node/' . $node->id()) + ->condition('module', 'menu'); + $result = $query->execute(); + + if (!empty($result)) { + menu_link_delete_multiple($result); + } +} + +/** + * Implements hook_node_prepare_form(). + */ +function menu_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) { + if (empty($node->menu)) { + // Prepare the node for the edit form so that $node->menu always exists. + $menu_name = strtok(variable_get('menu_parent_' . $node->getType(), 'main:0'), ':'); + $menu_link = FALSE; + if ($node->id()) { + $mlid = FALSE; + // Give priority to the default menu + $type_menus = variable_get('menu_options_' . $node->getType(), array('main' => 'main')); + if (in_array($menu_name, $type_menus)) { + $query = Drupal::entityQuery('menu_link') + ->condition('link_path', 'node/' . $node->id()) + ->condition('menu_name', $menu_name) + ->condition('module', 'menu') + ->sort('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result)) ? reset($result) : FALSE; + } + // Check all allowed menus if a link does not exist in the default menu. + if (!$mlid && !empty($type_menus)) { + $query = Drupal::entityQuery('menu_link') + ->condition('link_path', 'node/' . $node->id()) + ->condition('menu_name', array_values($type_menus), 'IN') + ->condition('module', 'menu') + ->sort('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result)) ? reset($result) : FALSE; + } + if ($mlid) { + $menu_link = menu_link_load($mlid); + } + } + + if (!$menu_link) { + $menu_link = entity_create('menu_link', array( + 'mlid' => 0, + 'plid' => 0, + 'menu_name' => $menu_name, + )); + } + // Set default values. + $node->menu = $menu_link; + } + // Find the depth limit for the parent select. + if (!isset($node->menu['parent_depth_limit'])) { + $node->menu['parent_depth_limit'] = _menu_parent_depth_limit($node->menu); + } +} + +/** + * Find the depth limit for items in the parent select. + */ +function _menu_parent_depth_limit($item) { + return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($item) : 0); +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + * + * Adds menu item fields to the node form. + * + * @see menu_node_submit() + */ +function menu_form_node_form_alter(&$form, $form_state) { + // Generate a list of possible parents (not including this link or descendants). + // @todo This must be handled in a #process handler. + $node = $form_state['controller']->getEntity(); + $link = $node->menu; + $type = $node->getType(); + $options = menu_parent_options(menu_get_menus(), $link, $type); + // If no possible parent menu items were found, there is nothing to display. + if (empty($options)) { + return; + } + + $form['menu'] = array( + '#type' => 'details', + '#title' => t('Menu settings'), + '#access' => user_access('administer menu'), + '#collapsed' => !$link['link_title'], + '#group' => 'advanced', + '#attached' => array( + 'library' => array(array('menu', 'drupal.menu')), + ), + '#tree' => TRUE, + '#weight' => -2, + '#attributes' => array('class' => array('menu-link-form')), + ); + $form['menu']['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Provide a menu link'), + '#default_value' => (int) (bool) $link['mlid'], + ); + $form['menu']['link'] = array( + '#type' => 'container', + '#parents' => array('menu'), + '#states' => array( + 'invisible' => array( + 'input[name="menu[enabled]"]' => array('checked' => FALSE), + ), + ), + ); + + // Populate the element with the link data. + foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden', 'parent_depth_limit') as $key) { + $form['menu']['link'][$key] = array('#type' => 'value', '#value' => $link[$key]); + } + + $form['menu']['link']['link_title'] = array( + '#type' => 'textfield', + '#title' => t('Menu link title'), + '#default_value' => $link['link_title'], + ); + + $form['menu']['link']['description'] = array( + '#type' => 'textarea', + '#title' => t('Description'), + '#default_value' => isset($link['options']['attributes']['title']) ? $link['options']['attributes']['title'] : '', + '#rows' => 1, + '#description' => t('Shown when hovering over the menu link.'), + ); + + $default = ($link['mlid'] ? $link['menu_name'] . ':' . $link['plid'] : variable_get('menu_parent_' . $type, 'main:0')); + // If the current parent menu item is not present in options, use the first + // available option as default value. + // @todo User should not be allowed to access menu link settings in such a + // case. + if (!isset($options[$default])) { + $array = array_keys($options); + $default = reset($array); + } + $form['menu']['link']['parent'] = array( + '#type' => 'select', + '#title' => t('Parent item'), + '#default_value' => $default, + '#options' => $options, + '#attributes' => array('class' => array('menu-parent-select')), + ); + + // Get number of items in menu so the weight selector is sized appropriately. + $delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name); + if ($delta < 50) { + // Old hardcoded value + $delta = 50; + } + $form['menu']['link']['weight'] = array( + '#type' => 'weight', + '#title' => t('Weight'), + '#delta' => $delta, + '#default_value' => $link['weight'], + '#description' => t('Menu links with smaller weights are displayed before links with larger weights.'), + ); +} + +/** + * Implements hook_node_submit(). + * + * @see menu_form_node_form_alter() + */ +function menu_node_submit(EntityInterface $node, $form, $form_state) { + $node->menu = entity_create('menu_link', $form_state['values']['menu']); + // Decompose the selected menu parent option into 'menu_name' and 'plid', if + // the form used the default parent selection widget. + if (!empty($form_state['values']['menu']['parent'])) { + list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Adds menu options to the node type form. + */ +function menu_form_node_type_form_alter(&$form, $form_state) { + $menu_options = menu_get_menus(); + $type = $form_state['controller']->getEntity(); + $form['menu'] = array( + '#type' => 'details', + '#title' => t('Menu settings'), + '#collapsed' => TRUE, + '#attached' => array( + 'library' => array(array('menu', 'drupal.menu.admin')), + ), + '#group' => 'additional_settings', + ); + $form['menu']['menu_options'] = array( + '#type' => 'checkboxes', + '#title' => t('Available menus'), + '#default_value' => variable_get('menu_options_' . $type->id(), array('main')), + '#options' => $menu_options, + '#description' => t('The menus available to place links in for this content type.'), + ); + // To avoid an 'illegal option' error after saving the form we have to load + // all available menu items. + // Otherwise it is not possible to dynamically add options to the list. + // @todo Convert menu_parent_options() into a #process callback. + $menu_link = entity_create('menu_link', array('mlid' => 0)); + $options = menu_parent_options(menu_get_menus(), $menu_link); + $form['menu']['menu_parent'] = array( + '#type' => 'select', + '#title' => t('Default parent item'), + '#default_value' => variable_get('menu_parent_' . $type->id(), 'main:0'), + '#options' => $options, + '#description' => t('Choose the menu item to be the default parent for a new link in the content authoring form.'), + '#attributes' => array('class' => array('menu-title-select')), + ); + + // Call Drupal.menuUpdateParentList() to filter the list of + // available default parent menu items based on the selected menus. + drupal_add_js( + '(function ($) { Drupal.menuUpdateParentList(); })(jQuery);', + array('scope' => 'footer', 'type' => 'inline') + ); +} + +/** + * Return an associative array of the custom menus names. + * + * @param $all + * If FALSE return only user-added menus, or if TRUE also include + * the menus defined by the system. + * @return + * An array with the machine-readable names as the keys, and human-readable + * titles as the values. + */ +function menu_get_menus($all = TRUE) { + if ($custom_menus = entity_load_multiple('menu')) { + if (!$all) { + $custom_menus = array_diff_key($custom_menus, menu_list_system_menus()); + } + foreach ($custom_menus as $menu_name => $menu) { + $custom_menus[$menu_name] = $menu->label(); + } + asort($custom_menus); + } + return $custom_menus; +} + +/** + * Implements hook_preprocess_HOOK() for block.html.twig. + */ +function menu_preprocess_block(&$variables) { + if ($variables['configuration']['module'] == 'menu') { + $variables['attributes']['role'] = 'navigation'; + } +} + +/** + * Implements hook_library_info(). + */ +function menu_library_info() { + $libraries['drupal.menu'] = array( + 'title' => 'Menu', + 'version' => Drupal::VERSION, + 'js' => array( + drupal_get_path('module', 'menu') . '/menu.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupal.form'), + ), + ); + $libraries['drupal.menu.admin'] = array( + 'title' => 'Menu admin', + 'version' => Drupal::VERSION, + 'js' => array( + drupal_get_path('module', 'menu') . '/menu.admin.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + ), + ); + + return $libraries; +}