Issue #916388 by disasm, podarok, dawehner, andypost, amateescu, Dave Reid: Convert menu links into entities.

8.0.x
webchick 2013-02-08 15:55:25 -08:00
parent 167e70ba00
commit e72ff70409
42 changed files with 2292 additions and 1585 deletions

View File

@ -243,6 +243,11 @@ Locale module
Menu module
- ?
Menu Link module
- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
- Károly Négyesi 'chx' http://drupal.org/user/9446
- @todo Anyone else from the menu system?
Node module
- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
- David Strauss 'David Strauss' http://drupal.org/user/93254

View File

@ -5,8 +5,11 @@
* API for the Drupal menu system.
*/
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Template\Attribute;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
use Drupal\menu_link\MenuLinkStorageController;
/**
* @defgroup menu Menu system
@ -264,6 +267,9 @@ 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;
@ -1260,20 +1266,15 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail =
// Collect all the links set to be expanded, and then add all of
// their children to the list as well.
do {
$result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
->fields('menu_links', array('mlid'))
$query = entity_query('menu_link')
->condition('menu_name', $menu_name)
->condition('expanded', 1)
->condition('has_children', 1)
->condition('plid', $parents, 'IN')
->condition('mlid', $parents, 'NOT IN')
->execute();
$num_rows = FALSE;
foreach ($result as $item) {
$parents[$item['mlid']] = $item['mlid'];
$num_rows = TRUE;
}
} while ($num_rows);
->condition('mlid', $parents, 'NOT IN');
$result = $query->execute();
$parents += $result;
} while (!empty($result));
}
$tree_parameters['expanded'] = $parents;
$tree_parameters['active_trail'] = $active_trail;
@ -1360,48 +1361,23 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
}
if (!isset($trees[$tree_cid])) {
// Select the links from the table, and recursively build the tree. We
// LEFT JOIN since there is no match in {menu_router} for an external
// link.
$query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
$query->addTag('translatable');
$query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
$query->fields('ml');
$query->fields('m', array(
'load_functions',
'to_arg_functions',
'access_callback',
'access_arguments',
'page_callback',
'page_arguments',
'tab_parent',
'tab_root',
'title',
'title_callback',
'title_arguments',
'theme_callback',
'theme_arguments',
'type',
'description',
'description_callback',
'description_arguments',
));
$query = entity_query('menu_link');
for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
$query->orderBy('p' . $i, 'ASC');
$query->sort('p' . $i, 'ASC');
}
$query->condition('ml.menu_name', $menu_name);
$query->condition('menu_name', $menu_name);
if (!empty($parameters['expanded'])) {
$query->condition('ml.plid', $parameters['expanded'], 'IN');
$query->condition('plid', $parameters['expanded'], 'IN');
}
elseif (!empty($parameters['only_active_trail'])) {
$query->condition('ml.mlid', $parameters['active_trail'], 'IN');
$query->condition('mlid', $parameters['active_trail'], 'IN');
}
$min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
if ($min_depth != 1) {
$query->condition('ml.depth', $min_depth, '>=');
$query->condition('depth', $min_depth, '>=');
}
if (isset($parameters['max_depth'])) {
$query->condition('ml.depth', $parameters['max_depth'], '<=');
$query->condition('depth', $parameters['max_depth'], '<=');
}
// Add custom query conditions, if any were passed.
if (isset($parameters['conditions'])) {
@ -1412,8 +1388,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
// Build an ordered array of links using the query result object.
$links = array();
foreach ($query->execute() as $item) {
$links[] = $item;
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);
@ -2450,18 +2426,11 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
// Put the selected menu at the front of the list.
array_unshift($menu_names, $selected_menu);
$query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
$query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
$query->fields('ml');
// Weight must be taken from {menu_links}, not {menu_router}.
$query->addField('ml', 'weight', 'link_weight');
$query->fields('m');
$query->condition('ml.link_path', $path_candidates, 'IN');
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path_candidates));
// Sort candidates by link path and menu name.
$candidates = array();
foreach ($query->execute() as $candidate) {
$candidate['weight'] = $candidate['link_weight'];
foreach ($menu_links as $candidate) {
$candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
// Add any menus not already in the menu name search list.
if (!in_array($candidate['menu_name'], $menu_names)) {
@ -2580,38 +2549,6 @@ function menu_get_active_title() {
}
}
/**
* Gets a translated, access-checked menu link that is ready for rendering.
*
* This function should never be called from within node_load() or any other
* function used as a menu object load function since an infinite recursion may
* occur.
*
* @param $mlid
* The mlid of the menu item.
*
* @return
* A menu link, with $item['access'] filled and link translated for
* rendering.
*/
function menu_link_load($mlid) {
if (is_numeric($mlid)) {
$query = db_select('menu_links', 'ml');
$query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
$query->fields('ml');
// Weight should be taken from {menu_links}, not {menu_router}.
$query->addField('ml', 'weight', 'link_weight');
$query->fields('m');
$query->condition('ml.mlid', $mlid);
if ($item = $query->execute()->fetchAssoc()) {
$item['weight'] = $item['link_weight'];
_menu_link_translate($item);
return $item;
}
}
return FALSE;
}
/**
* Clears the cached cached data for a single named menu.
*/
@ -2636,6 +2573,8 @@ function menu_cache_clear_all() {
* Resets the menu system static cache.
*/
function menu_reset_static_cache() {
drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link')->resetCache();
drupal_static_reset('_menu_build_tree');
drupal_static_reset('menu_tree');
drupal_static_reset('menu_tree_all_data');
@ -2732,157 +2671,118 @@ function menu_get_router() {
return $menu;
}
/**
* Builds a link from a router item.
*/
function _menu_link_build($item) {
// Suggested items are disabled by default.
if ($item['type'] == MENU_SUGGESTED_ITEM) {
$item['hidden'] = 1;
}
// Hide all items that are not visible in the tree.
elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
$item['hidden'] = -1;
}
// Note, we set this as 'system', so that we can be sure to distinguish all
// the menu links generated automatically from entries in {menu_router}.
$item['module'] = 'system';
$item += array(
'menu_name' => 'tools',
'link_title' => $item['title'],
'link_path' => $item['path'],
'hidden' => 0,
'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
);
return $item;
}
/**
* Builds menu links for the items in the menu router.
*
* @todo This function should be removed/refactored.
*/
function _menu_navigation_links_rebuild($menu) {
if (module_exists('menu_link')) {
$menu_link_controller = drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link');
}
else {
// The Menu link module is not available at install time, so we need to
// hardcode the default storage controller.
$menu_link_controller = new MenuLinkStorageController('menu_link');
}
// Add normal and suggested items as links.
$menu_links = array();
foreach ($menu as $path => $item) {
if ($item['_visible']) {
$menu_links[$path] = $item;
$sort[$path] = $item['_number_parts'];
$router_items = array();
foreach ($menu as $path => $router_item) {
if ($router_item['_visible']) {
$router_items[$path] = $router_item;
$sort[$path] = $router_item['_number_parts'];
}
}
if ($menu_links) {
// Keep an array of processed menu links, to allow menu_link_save() to
// check this for parents instead of querying the database.
if ($router_items) {
// Keep an array of processed menu links, to allow
// Drupal\menu_link\MenuLinkStorageController::save() to check this for
// parents instead of querying the database.
$parent_candidates = array();
// Make sure no child comes before its parent.
array_multisort($sort, SORT_NUMERIC, $menu_links);
array_multisort($sort, SORT_NUMERIC, $router_items);
foreach ($menu_links as $key => $item) {
foreach ($router_items as $key => $router_item) {
// For performance reasons, do a straight query now and convert to a menu
// link entity later.
// @todo revisit before release.
$existing_item = db_select('menu_links')
->fields('menu_links')
->condition('link_path', $item['path'])
->condition('link_path', $router_item['path'])
->condition('module', 'system')
->execute()->fetchAssoc();
->execute()->fetchAll();
if ($existing_item) {
$item['mlid'] = $existing_item['mlid'];
$existing_item = reset($existing_item);
$existing_item->options = unserialize($existing_item->options);
$router_item['mlid'] = $existing_item->mlid;
$router_item['uuid'] = $existing_item->uuid;
// A change in hook_menu may move the link to a different menu
if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) {
$item['menu_name'] = $existing_item['menu_name'];
$item['plid'] = $existing_item['plid'];
if (empty($router_item['menu_name']) || ($router_item['menu_name'] == $existing_item->menu_name)) {
$router_item['menu_name'] = $existing_item->menu_name;
$router_item['plid'] = $existing_item->plid;
}
else {
// It moved to a new menu. Let menu_link_save() try to find a new
// parent based on the path.
unset($item['plid']);
// It moved to a new menu.
// Let Drupal\menu_link\MenuLinkStorageController::save() try to find
// a new parent based on the path.
unset($router_item['plid']);
}
$item['has_children'] = $existing_item['has_children'];
$item['updated'] = $existing_item['updated'];
}
if ($existing_item && $existing_item['customized']) {
$parent_candidates[$existing_item['mlid']] = $existing_item;
$router_item['has_children'] = $existing_item->has_children;
$router_item['updated'] = $existing_item->updated;
// Convert the existing item to a typed object.
$existing_item = $menu_link_controller->create(get_object_vars($existing_item));
}
else {
$item = _menu_link_build($item);
menu_link_save($item, $existing_item, $parent_candidates);
$parent_candidates[$item['mlid']] = $item;
unset($menu_links[$key]);
$existing_item = NULL;
}
if ($existing_item && $existing_item->customized) {
$parent_candidates[$existing_item->mlid] = $existing_item;
}
else {
$menu_link = MenuLink::buildFromRouterItem($router_item);
$menu_link->original = $existing_item;
$menu_link->parentCandidates = $parent_candidates;
$menu_link_controller->save($menu_link);
$parent_candidates[$menu_link->id()] = $menu_link;
unset($router_items[$key]);
}
}
}
$paths = array_keys($menu);
// Updated and customized items whose router paths are gone need new ones.
$result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
->fields('menu_links', array(
'link_path',
'mlid',
'router_path',
'updated',
))
->condition(db_or()
->condition('updated', 1)
->condition(db_and()
->condition('router_path', $paths, 'NOT IN')
->condition('external', 0)
->condition('customized', 1)
)
)
->execute();
foreach ($result as $item) {
$router_path = _menu_find_router_path($item['link_path']);
if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
$menu_links = $menu_link_controller->loadUpdatedCustomized($paths);
foreach ($menu_links as $menu_link) {
$router_path = _menu_find_router_path($menu_link->link_path);
if (!empty($router_path) && ($router_path != $menu_link->router_path || $menu_link->updated)) {
// If the router path and the link path matches, it's surely a working
// item, so we clear the updated flag.
$updated = $item['updated'] && $router_path != $item['link_path'];
db_update('menu_links')
->fields(array(
'router_path' => $router_path,
'updated' => (int) $updated,
))
->condition('mlid', $item['mlid'])
->execute();
$updated = $menu_link->updated && $router_path != $menu_link->link_path;
$menu_link->router_path = $router_path;
$menu_link->updated = (int) $updated;
$menu_link_controller->save($menu_link);
}
}
// Find any item whose router path does not exist any more.
$result = db_select('menu_links')
->fields('menu_links')
$query = entity_query('menu_link')
->condition('router_path', $paths, 'NOT IN')
->condition('external', 0)
->condition('updated', 0)
->condition('customized', 0)
->orderBy('depth', 'DESC')
->execute();
// Remove all such items. Starting from those with the greatest depth will
// minimize the amount of re-parenting done by menu_link_delete().
foreach ($result as $item) {
_menu_delete_item($item, TRUE);
}
}
->sort('depth', 'DESC');
$result = $query->execute();
/**
* Clones an array of menu links.
*
* @param $links
* An array of menu links to clone.
* @param $menu_name
* (optional) The name of a menu that the links will be cloned for. If not
* set, the cloned links will be in the same menu as the original set of
* links that were passed in.
*
* @return
* An array of menu links with the same properties as the passed-in array,
* but with the link identifiers removed so that a new link will be created
* when any of them is passed in to menu_link_save().
*
* @see menu_link_save()
*/
function menu_links_clone($links, $menu_name = NULL) {
foreach ($links as &$link) {
unset($link['mlid']);
unset($link['plid']);
if (isset($menu_name)) {
$link['menu_name'] = $menu_name;
}
// Remove all such items. Starting from those with the greatest depth will
// minimize the amount of re-parenting done by the menu link controller.
if (!empty($result)) {
menu_link_delete_multiple($result, TRUE);
}
return $links;
}
/**
@ -2895,18 +2795,19 @@ function menu_links_clone($links, $menu_name = NULL) {
* An array of menu links.
*/
function menu_load_links($menu_name) {
$links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
->fields('ml')
->condition('ml.menu_name', $menu_name)
$links = array();
$query = entity_query('menu_link')
->condition('menu_name', $menu_name)
// Order by weight so as to be helpful for menus that are only one level
// deep.
->orderBy('weight')
->execute()
->fetchAll();
->sort('weight');
$result = $query->execute();
foreach ($links as &$link) {
$link['options'] = unserialize($link['options']);
if (!empty($result)) {
$links = menu_link_load_multiple($result);
}
return $links;
}
@ -2918,333 +2819,7 @@ function menu_load_links($menu_name) {
*/
function menu_delete_links($menu_name) {
$links = menu_load_links($menu_name);
foreach ($links as $link) {
// To speed up the deletion process, we reset some link properties that
// would trigger re-parenting logic in _menu_delete_item() and
// _menu_update_parental_status().
$link['has_children'] = FALSE;
$link['plid'] = 0;
_menu_delete_item($link);
}
}
/**
* Delete one or several menu links.
*
* @param $mlid
* A valid menu link mlid or NULL. If NULL, $path is used.
* @param $path
* The path to the menu items to be deleted. $mlid must be NULL.
*/
function menu_link_delete($mlid, $path = NULL) {
if (isset($mlid)) {
_menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc());
}
else {
$result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path));
foreach ($result as $link) {
_menu_delete_item($link);
}
}
}
/**
* Deletes a single menu link.
*
* @param $item
* Item to be deleted.
* @param $force
* Forces deletion. Internal use only, setting to TRUE is discouraged.
*
* @see menu_link_delete()
*/
function _menu_delete_item($item, $force = FALSE) {
$item = is_object($item) ? get_object_vars($item) : $item;
if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
// Children get re-attached to the item's parent.
if ($item['has_children']) {
$result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid']));
foreach ($result as $m) {
$child = menu_link_load($m->mlid);
$child['plid'] = $item['plid'];
menu_link_save($child);
}
}
// Notify modules we are deleting the item.
module_invoke_all('menu_link_delete', $item);
db_delete('menu_links')->condition('mlid', $item['mlid'])->execute();
// Update the has_children status of the parent.
_menu_update_parental_status($item);
menu_cache_clear($item['menu_name']);
_menu_clear_page_cache();
}
}
/**
* Saves a menu link.
*
* After calling this function, rebuild the menu cache using
* menu_cache_clear_all().
*
* @param $item
* An associative array representing a menu link item, with elements:
* - link_path: (required) The path of the menu item, which should be
* normalized first by calling drupal_container()->get('path.alias_manager')->getSystemPath() on it.
* - link_title: (required) Title to appear in menu for the link.
* - menu_name: (optional) The machine name of the menu for the link.
* Defaults to 'tools'.
* - weight: (optional) Integer to determine position in menu. Default is 0.
* - expanded: (optional) Boolean that determines if the item is expanded.
* - options: (optional) An array of options, see l() for more.
* - mlid: (optional) Menu link identifier, the primary integer key for each
* menu link. Can be set to an existing value, or to 0 or NULL
* to insert a new link.
* - plid: (optional) The mlid of the parent.
* - router_path: (optional) The path of the relevant router item.
* @param $existing_item
* Optional, the current record from the {menu_links} table as an array.
* @param $parent_candidates
* Optional array of menu links keyed by mlid. Used by
* _menu_navigation_links_rebuild() only.
*
* @return
* The mlid of the saved menu link, or FALSE if the menu link could not be
* saved.
*/
function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) {
drupal_alter('menu_link', $item);
// This is the easiest way to handle the unique internal path '<front>',
// since a path marked as external does not need to match a router path.
$item['external'] = (url_is_external($item['link_path']) || $item['link_path'] == '<front>') ? 1 : 0;
// Load defaults.
$item += array(
'menu_name' => 'tools',
'weight' => 0,
'link_title' => '',
'hidden' => 0,
'has_children' => 0,
'expanded' => 0,
'options' => array(),
'module' => 'menu',
'customized' => 0,
'updated' => 0,
);
if (isset($item['mlid'])) {
if (!$existing_item) {
$existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc();
}
if ($existing_item) {
$existing_item['options'] = unserialize($existing_item['options']);
}
}
else {
$existing_item = FALSE;
}
// Try to find a parent link. If found, assign it and derive its menu.
$parent = _menu_link_find_parent($item, $parent_candidates);
if (!empty($parent['mlid'])) {
$item['plid'] = $parent['mlid'];
$item['menu_name'] = $parent['menu_name'];
}
// If no corresponding parent link was found, move the link to the top-level.
else {
$item['plid'] = 0;
}
$menu_name = $item['menu_name'];
if (!$existing_item) {
$item['mlid'] = db_insert('menu_links')
->fields(array(
'menu_name' => $item['menu_name'],
'plid' => $item['plid'],
'link_path' => $item['link_path'],
'hidden' => $item['hidden'],
'external' => $item['external'],
'has_children' => $item['has_children'],
'expanded' => $item['expanded'],
'weight' => $item['weight'],
'module' => $item['module'],
'link_title' => $item['link_title'],
'options' => serialize($item['options']),
'customized' => $item['customized'],
'updated' => $item['updated'],
))
->execute();
}
// Directly fill parents for top-level links.
if ($item['plid'] == 0) {
$item['p1'] = $item['mlid'];
for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
$item["p$i"] = 0;
}
$item['depth'] = 1;
}
// Otherwise, ensure that this link's depth is not beyond the maximum depth
// and fill parents based on the parent link.
else {
if ($item['has_children'] && $existing_item) {
$limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
}
else {
$limit = MENU_MAX_DEPTH - 1;
}
if ($parent['depth'] > $limit) {
return FALSE;
}
$item['depth'] = $parent['depth'] + 1;
_menu_link_parents_set($item, $parent);
}
// Need to check both plid and menu_name, since plid can be 0 in any menu.
if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
_menu_link_move_children($item, $existing_item);
}
// Find the router_path.
if (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) {
if ($item['external']) {
$item['router_path'] = '';
}
else {
// Find the router path which will serve this path.
$item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
$item['router_path'] = _menu_find_router_path($item['link_path']);
}
}
// If every value in $existing_item is the same in the $item, there is no
// reason to run the update queries or clear the caches. We use
// array_intersect_key() with the $item as the first parameter because
// $item may have additional keys left over from building a router entry.
// The intersect removes the extra keys, allowing a meaningful comparison.
if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) {
db_update('menu_links')
->fields(array(
'menu_name' => $item['menu_name'],
'plid' => $item['plid'],
'link_path' => $item['link_path'],
'router_path' => $item['router_path'],
'hidden' => $item['hidden'],
'external' => $item['external'],
'has_children' => $item['has_children'],
'expanded' => $item['expanded'],
'weight' => $item['weight'],
'depth' => $item['depth'],
'p1' => $item['p1'],
'p2' => $item['p2'],
'p3' => $item['p3'],
'p4' => $item['p4'],
'p5' => $item['p5'],
'p6' => $item['p6'],
'p7' => $item['p7'],
'p8' => $item['p8'],
'p9' => $item['p9'],
'module' => $item['module'],
'link_title' => $item['link_title'],
'options' => serialize($item['options']),
'customized' => $item['customized'],
))
->condition('mlid', $item['mlid'])
->execute();
// Check the has_children status of the parent.
_menu_update_parental_status($item);
menu_cache_clear($menu_name);
if ($existing_item && $menu_name != $existing_item['menu_name']) {
menu_cache_clear($existing_item['menu_name']);
}
// Notify modules we have acted on a menu item.
$hook = 'menu_link_insert';
if ($existing_item) {
$hook = 'menu_link_update';
}
module_invoke_all($hook, $item);
// Now clear the cache.
_menu_clear_page_cache();
}
return $item['mlid'];
}
/**
* Finds a possible parent for a given menu link.
*
* Because the parent of a given link might not exist anymore in the database,
* we apply a set of heuristics to determine a proper parent:
*
* - use the passed parent link if specified and existing.
* - else, use the first existing link down the previous link hierarchy
* - else, for system menu links (derived from hook_menu()), reparent
* based on the path hierarchy.
*
* @param $menu_link
* A menu link.
* @param $parent_candidates
* An array of menu links keyed by mlid.
*
* @return
* A menu link structure of the possible parent or FALSE if no valid parent
* has been found.
*/
function _menu_link_find_parent($menu_link, $parent_candidates = array()) {
$parent = FALSE;
// This item is explicitely top-level, skip the rest of the parenting.
if (isset($menu_link['plid']) && empty($menu_link['plid'])) {
return $parent;
}
// If we have a parent link ID, try to use that.
$candidates = array();
if (isset($menu_link['plid'])) {
$candidates[] = $menu_link['plid'];
}
// Else, if we have a link hierarchy try to find a valid parent in there.
if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) {
for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) {
$candidates[] = $menu_link['p' . $depth];
}
}
foreach ($candidates as $mlid) {
if (isset($parent_candidates[$mlid])) {
$parent = $parent_candidates[$mlid];
}
else {
$parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc();
}
if ($parent) {
return $parent;
}
}
// If everything else failed, try to derive the parent from the path
// hierarchy. This only makes sense for links derived from menu router
// items (ie. from hook_menu()).
if ($menu_link['module'] == 'system') {
$query = db_select('menu_links');
$query->condition('module', 'system');
// We always respect the link's 'menu_name'; inheritance for router items is
// ensured in _menu_router_build().
$query->condition('menu_name', $menu_link['menu_name']);
// Find the parent - it must be unique.
$parent_path = $menu_link['link_path'];
do {
$parent = FALSE;
$parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
$new_query = clone $query;
$new_query->condition('link_path', $parent_path);
// Only valid if we get a unique result.
if ($new_query->countQuery()->execute()->fetchField() == 1) {
$parent = $new_query->fields('menu_links')->execute()->fetchAssoc();
}
} while ($parent === FALSE && $parent_path);
}
return $parent;
menu_link_delete_multiple(array_keys($links), FALSE, TRUE);
}
/**
@ -3319,188 +2894,6 @@ function _menu_find_router_path($link_path) {
return $router_path;
}
/**
* Inserts, updates, enables, disables, or deletes an uncustomized menu link.
*
* @param string $module
* The name of the module that owns the link.
* @param string $op
* Operation to perform: insert, update, enable, disable, or delete.
* @param string $link_path
* The path this link points to.
* @param string $link_title
* (optional) Title of the link to insert or new title to update the link to.
* Unused for delete.
*
* @return integer|null
* The insert op returns the mlid of the new item. Others op return NULL.
*/
function menu_link_maintain($module, $op, $link_path, $link_title = NULL) {
switch ($op) {
case 'insert':
$menu_link = array(
'link_title' => $link_title,
'link_path' => $link_path,
'module' => $module,
);
return menu_link_save($menu_link);
case 'update':
$result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $link) {
$existing = $link;
if (isset($link_title)) {
$link['link_title'] = $link_title;
}
$link['options'] = unserialize($link['options']);
menu_link_save($link, $existing);
}
break;
case 'enable':
case 'disable':
$result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $link) {
$existing = $link;
$link['hidden'] = ($op == 'disable' ? 1 : 0);
$link['customized'] = 1;
if (isset($link_title)) {
$link['link_title'] = $link_title;
}
$link['options'] = unserialize($link['options']);
menu_link_save($link, $existing);
}
break;
case 'delete':
menu_link_delete(NULL, $link_path);
break;
}
}
/**
* Finds the depth of an item's children relative to its depth.
*
* For example, if the item has a depth of 2, and the maximum of any child in
* the menu link tree is 5, the relative depth is 3.
*
* @param $item
* An array representing a menu link item.
*
* @return
* The relative depth, or zero.
*
*/
function menu_link_children_relative_depth($item) {
$query = db_select('menu_links');
$query->addField('menu_links', 'depth');
$query->condition('menu_name', $item['menu_name']);
$query->orderBy('depth', 'DESC');
$query->range(0, 1);
$i = 1;
$p = 'p1';
while ($i <= MENU_MAX_DEPTH && $item[$p]) {
$query->condition($p, $item[$p]);
$p = 'p' . ++$i;
}
$max_depth = $query->execute()->fetchField();
return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
}
/**
* Updates the children of a menu link that is being moved.
*
* The menu name, parents (p1 - p6), and depth are updated for all children of
* the link, and the has_children status of the previous parent is updated.
*/
function _menu_link_move_children($item, $existing_item) {
$query = db_update('menu_links');
$query->fields(array('menu_name' => $item['menu_name']));
$p = 'p1';
$expressions = array();
for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) {
$expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p]));
}
$j = $existing_item['depth'] + 1;
while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 'p' . $j++, array());
}
while ($i <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 0, array());
}
$shift = $item['depth'] - $existing_item['depth'];
if ($shift > 0) {
// The order of expressions must be reversed so the new values don't
// overwrite the old ones before they can be used because "Single-table
// UPDATE assignments are generally evaluated from left to right"
// see: http://dev.mysql.com/doc/refman/5.0/en/update.html
$expressions = array_reverse($expressions);
}
foreach ($expressions as $expression) {
$query->expression($expression[0], $expression[1], $expression[2]);
}
$query->expression('depth', 'depth + :depth', array(':depth' => $shift));
$query->condition('menu_name', $existing_item['menu_name']);
$p = 'p1';
for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) {
$query->condition($p, $existing_item[$p]);
}
$query->execute();
// Check the has_children status of the parent, while excluding this item.
_menu_update_parental_status($existing_item, TRUE);
}
/**
* Checks and updates the 'has_children' status for the parent of a link.
*/
function _menu_update_parental_status($item, $exclude = FALSE) {
// If plid == 0, there is nothing to update.
if ($item['plid']) {
// Check if at least one visible child exists in the table.
$query = db_select('menu_links');
$query->addField('menu_links', 'mlid');
$query->condition('menu_name', $item['menu_name']);
$query->condition('hidden', 0);
$query->condition('plid', $item['plid']);
$query->range(0, 1);
if ($exclude) {
$query->condition('mlid', $item['mlid'], '<>');
}
$parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
db_update('menu_links')
->fields(array('has_children' => $parent_has_children))
->condition('mlid', $item['plid'])
->execute();
}
}
/**
* Sets the p1 through p9 values for a menu link being saved.
*/
function _menu_link_parents_set(&$item, $parent) {
$i = 1;
while ($i < $item['depth']) {
$p = 'p' . $i++;
$item[$p] = $parent[$p];
}
$p = 'p' . $i++;
// The parent (p1 - p9) corresponding to the depth always equals the mlid.
$item[$p] = $item['mlid'];
while ($i <= MENU_MAX_DEPTH) {
$p = 'p' . $i++;
$item[$p] = 0;
}
}
/**
* Builds the router table based on the data from hook_menu().
*/

View File

@ -389,7 +389,7 @@ function aggregator_save_category($edit) {
->execute();
$op = 'insert';
}
if (isset($op)) {
if (isset($op) && module_exists('menu_link')) {
menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
}
}

View File

@ -37,8 +37,8 @@ class CategorizeFeedItemTest extends AggregatorTestBase {
$this->assertTrue(!empty($category), 'The category found in database.');
$link_path = 'aggregator/categories/' . $category->cid;
$menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch();
$this->assertTrue(!empty($menu_link), 'The menu link associated with the category found in database.');
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path));
$this->assertTrue(!empty($menu_links), 'The menu link associated with the category found in database.');
$feed = $this->createFeed();
db_insert('aggregator_category_feed')

View File

@ -3,5 +3,6 @@ description = Allows users to create and organize related content in an outline.
package = Core
version = VERSION
core = 8.x
dependencies[] = menu_link
dependencies[] = node
configure = admin/content/book/settings

View File

@ -8,6 +8,8 @@
use Drupal\node\Plugin\Core\Entity\Node;
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
use Drupal\Core\Template\Attribute;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
use Drupal\menu_link\MenuLinkStorageController;
/**
* Implements hook_help().
@ -558,7 +560,8 @@ function _book_update_outline(Node $node) {
}
}
if (menu_link_save($node->book)) {
$node->book = entity_create('menu_link', $node->book);
if ($node->book->save()) {
if ($new) {
// Insert new.
db_insert('book')
@ -926,7 +929,7 @@ function book_node_prepare(Node $node) {
* The depth limit for items in the parent select.
*/
function _book_parent_depth_limit($book_link) {
return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0);
return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($book_link) : 0);
}
/**

View File

@ -105,12 +105,12 @@ class MenuNodeTest extends WebTestBase {
$this->assertNoLink($node_title);
// Add a menu link to the Administration menu.
$item = array(
$item = entity_create('menu_link', array(
'link_path' => 'node/' . $node->nid,
'link_title' => $this->randomName(16),
'menu_name' => 'admin',
);
menu_link_save($item);
));
$item->save();
// Assert that disabled Administration menu is not shown on the
// node/$nid/edit page.
@ -127,12 +127,12 @@ class MenuNodeTest extends WebTestBase {
// Create a second node.
$child_node = $this->drupalCreateNode(array('type' => 'article'));
// Assign a menu link to the second node, being a child of the first one.
$child_item = array(
$child_item = entity_create('menu_link', array(
'link_path' => 'node/'. $child_node->nid,
'link_title' => $this->randomName(16),
'plid' => $item['mlid'],
);
menu_link_save($child_item);
));
$child_item->save();
// Edit the first node.
$this->drupalGet('node/'. $node->nid .'/edit');
// Assert that it is not possible to set the parent of the first node to itself or the second node.

View File

@ -78,12 +78,12 @@ class MenuTest extends WebTestBase {
$item = $this->getStandardMenuLink();
$old_title = $item['link_title'];
$this->modifyMenuLink($item);
$item = menu_link_load($item['mlid']);
$item = entity_load('menu_link', $item['mlid']);
// Verify that a change to the description is saved.
$description = $this->randomName(16);
$item['options']['attributes']['title'] = $description;
menu_link_save($item);
$saved_item = menu_link_load($item['mlid']);
$saved_item = entity_load('menu_link', $item['mlid']);
$this->assertEqual($description, $saved_item['options']['attributes']['title'], 'Saving an existing link updates the description (title attribute)');
$this->resetMenuLink($item, $old_title);
}
@ -180,7 +180,7 @@ class MenuTest extends WebTestBase {
$this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $label)), 'Custom menu was deleted');
$this->assertFalse(menu_load($menu_name), 'Custom menu was deleted');
// Test if all menu links associated to the menu were removed from database.
$result = db_query("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField();
$result = entity_load_multiple_by_properties('menu_link', array('menu_name' => $menu_name));
$this->assertFalse($result, 'All menu links associated to the custom menu were deleted.');
}
@ -282,7 +282,9 @@ class MenuTest extends WebTestBase {
* @param string $link Link path.
* @param string $menu_name Menu name.
* @param string $weight Menu weight
* @return array Menu link created.
*
* @return \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*/
function addMenuLink($plid = 0, $link = '<front>', $menu_name = 'tools', $expanded = TRUE, $weight = '0') {
// View add menu link page.
@ -303,14 +305,14 @@ class MenuTest extends WebTestBase {
// Add menu link.
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertResponse(200);
// Unlike most other modules, there is no confirmation message displayed.
$this->assertText($title, 'Menu link was added');
$this->assertText('The menu link has been saved.');
$item = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => $title))->fetchAssoc();
$this->assertTrue(t('Menu link was found in database.'));
$this->assertMenuLink($item['mlid'], array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid));
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $title));
$menu_link = reset($menu_links);
$this->assertTrue('Menu link was found in database.');
$this->assertMenuLink($menu_link->id(), array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid));
return $item;
return $menu_link;
}
/**
@ -393,8 +395,7 @@ class MenuTest extends WebTestBase {
$edit['link_title'] = $title;
$this->drupalPost("admin/structure/menu/item/$mlid/edit", $edit, t('Save'));
$this->assertResponse(200);
// Unlike most other modules, there is no confirmation message displayed.
$this->assertText('The menu link has been saved.');
// Verify menu link.
$this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']);
$this->assertText($title, 'Menu link was edited');
@ -501,8 +502,8 @@ class MenuTest extends WebTestBase {
*/
function assertMenuLink($mlid, array $expected_item) {
// Retrieve menu link.
$item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array(':mlid' => $mlid))->fetchAssoc();
$options = unserialize($item['options']);
$item = entity_load('menu_link', $mlid);
$options = $item->options;
if (!empty($options['query'])) {
$item['link_path'] .= '?' . drupal_http_build_query($options['query']);
}
@ -518,12 +519,21 @@ class MenuTest extends WebTestBase {
* Get standard menu link.
*/
private function getStandardMenuLink() {
// Retrieve menu link id of the Log out menu link, which will always be on the front page.
$mlid = db_query("SELECT mlid FROM {menu_links} WHERE module = 'system' AND router_path = 'user/logout'")->fetchField();
$mlid = 0;
// Retrieve menu link id of the Log out menu link, which will always be on
// the front page.
$query = entity_query('menu_link')
->condition('module', 'system')
->condition('router_path', 'user/logout');
$result = $query->execute();
if (!empty($result)) {
$mlid = reset($result);
}
$this->assertTrue($mlid > 0, 'Standard menu link id was found');
// Load menu link.
// Use api function so that link is translated for rendering.
$item = menu_link_load($mlid);
$item = entity_load('menu_link', $mlid);
$this->assertTrue((bool) $item, 'Standard menu link was loaded');
return $item;
}

View File

@ -5,7 +5,7 @@
* Administrative page callbacks for menu module.
*/
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
use Drupal\system\Plugin\Core\Entity\Menu;
use Drupal\Component\Utility\NestedArray;
@ -71,16 +71,19 @@ function menu_overview_form($form, &$form_state) {
$form_state += array('menu_overview_form_parents' => array());
$form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.admin.css');
$sql = "
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.description_callback, m.description_arguments, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = :menu
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
$result = db_query($sql, array(':menu' => $form_state['menu']->id()), array('fetch' => PDO::FETCH_ASSOC));
$links = array();
foreach ($result as $item) {
$links[] = $item;
$query = entity_query('menu_link')
->condition('menu_name', $form_state['menu']->id());
for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
$query->sort('p' . $i, 'ASC');
}
$result = $query->execute();
if (!empty($result)) {
$links = menu_link_load_multiple($result);
}
$delta = max(count($links), 50);
$tree = menu_tree_data($links);
$node_links = array();
@ -305,197 +308,10 @@ function theme_menu_overview_form($variables) {
return $output;
}
/**
* Menu callback; Build the menu link editing form.
*/
function menu_edit_item($form, &$form_state, $type, $item, $menu) {
if ($type == 'add' || empty($item)) {
// This is an add form, initialize the menu link.
$item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu->id(), 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
}
else {
// Get the human-readable menu title from the given menu name.
$titles = menu_get_menus();
$current_title = $titles[$item['menu_name']];
// Get the current breadcrumb and add a link to that menu's overview page.
$breadcrumb = menu_get_active_breadcrumb();
$breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $item['menu_name']);
drupal_set_breadcrumb($breadcrumb);
}
$form['actions'] = array('#type' => 'actions');
$form['link_title'] = array(
'#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $item['link_title'],
'#description' => t('The text to be used for this link in the menu.'),
'#required' => TRUE,
);
foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
$form[$key] = array('#type' => 'value', '#value' => $item[$key]);
}
// Any item created or edited via this interface is considered "customized".
$form['customized'] = array('#type' => 'value', '#value' => 1);
$form['original_item'] = array('#type' => 'value', '#value' => $item);
$path = $item['link_path'];
if (isset($item['options']['query'])) {
$path .= '?' . drupal_http_build_query($item['options']['query']);
}
if (isset($item['options']['fragment'])) {
$path .= '#' . $item['options']['fragment'];
}
if ($item['module'] == 'menu') {
$form['link_path'] = array(
'#type' => 'textfield',
'#title' => t('Path'),
'#maxlength' => 255,
'#default_value' => $path,
'#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
'#required' => TRUE,
);
$form['actions']['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete'),
'#access' => $item['mlid'],
'#submit' => array('menu_item_delete_submit'),
'#weight' => 10,
);
}
else {
$form['_path'] = array(
'#type' => 'item',
'#title' => t('Path'),
'#description' => l($item['link_title'], $item['href'], $item['options']),
);
}
$form['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
'#rows' => 1,
'#description' => t('Shown when hovering over the menu link.'),
);
$form['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enabled'),
'#default_value' => !$item['hidden'],
'#description' => t('Menu links that are not enabled will not be listed in any menu.'),
);
$form['expanded'] = array(
'#type' => 'checkbox',
'#title' => t('Show as expanded'),
'#default_value' => $item['expanded'],
'#description' => t('If selected and this menu link has children, the menu will always appear expanded.'),
);
// Generate a list of possible parents (not including this link or descendants).
$options = menu_parent_options(menu_get_menus(), $item);
$default = $item['menu_name'] . ':' . $item['plid'];
if (!isset($options[$default])) {
$default = 'tools:0';
}
$form['parent'] = array(
'#type' => 'select',
'#title' => t('Parent link'),
'#default_value' => $default,
'#options' => $options,
'#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#attributes' => array('class' => array('menu-title-select')),
);
// Get number of items in menu so the weight selector is sized appropriately.
$sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu";
$result = db_query($sql, array(':menu' => $item['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $row) {
foreach ($row as $menu_item_count) {
$delta = $menu_item_count;
}
}
if ($delta < 50) {
// Old hardcoded value.
$delta = 50;
}
$form['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#delta' => $delta,
'#default_value' => $item['weight'],
'#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
);
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#button_type' => 'primary');
return $form;
}
/**
* Validate form values for a menu link being added or edited.
*/
function menu_edit_item_validate($form, &$form_state) {
$item = &$form_state['values'];
$normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($item['link_path']);
if ($item['link_path'] != $normal_path) {
drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $item['link_path'], '%normal_path' => $normal_path)));
$item['link_path'] = $normal_path;
}
if (!url_is_external($item['link_path'])) {
$parsed_link = parse_url($item['link_path']);
if (isset($parsed_link['query'])) {
$item['options']['query'] = drupal_get_query_array($parsed_link['query']);
}
else {
// Use unset() rather than setting to empty string
// to avoid redundant serialized data being stored.
unset($item['options']['query']);
}
if (isset($parsed_link['fragment'])) {
$item['options']['fragment'] = $parsed_link['fragment'];
}
else {
unset($item['options']['fragment']);
}
if (isset($parsed_link['path']) && $item['link_path'] != $parsed_link['path']) {
$item['link_path'] = $parsed_link['path'];
}
}
if (!trim($item['link_path']) || !drupal_valid_path($item['link_path'], TRUE)) {
form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $item['link_path'])));
}
}
/**
* Submit function for the delete button on the menu item editing form.
*/
function menu_item_delete_submit($form, &$form_state) {
$form_state['redirect'] = 'admin/structure/menu/item/' . $form_state['values']['mlid'] . '/delete';
}
/**
* Process menu and menu item add/edit form submissions.
*/
function menu_edit_item_submit($form, &$form_state) {
$item = &$form_state['values'];
// The value of "hidden" is the opposite of the value
// supplied by the "enabled" checkbox.
$item['hidden'] = (int) !$item['enabled'];
unset($item['enabled']);
$item['options']['attributes']['title'] = $item['description'];
list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
if (!menu_link_save($item)) {
drupal_set_message(t('There was an error saving the menu link.'), 'error');
}
else {
drupal_set_message(t('Your configuration has been saved.'));
}
$form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name'];
}
/**
* Menu callback; check access and get a confirm form for deletion of a custom menu.
*/
function menu_delete_menu_page($menu) {
function menu_delete_menu_page(Menu $menu) {
// System-defined menus may not be deleted.
$system_menus = menu_list_system_menus();
if (isset($system_menus[$menu->id()])) {
@ -510,7 +326,8 @@ function menu_delete_menu_page($menu) {
function menu_delete_menu_confirm($form, &$form_state, Menu $menu) {
$form['#menu'] = $menu;
$caption = '';
$num_links = db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu", array(':menu' => $menu->id()))->fetchField();
$num_links = drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link')->countMenuLinks($menu->id());
if ($num_links) {
$caption .= '<p>' . format_plural($num_links, '<strong>Warning:</strong> There is currently 1 menu link in %title. It will be deleted (system-defined items will be reset).', '<strong>Warning:</strong> There are currently @count menu links in %title. They will be deleted (system-defined links will be reset).', array('%title' => $menu->label())) . '</p>';
}
@ -532,16 +349,16 @@ function menu_delete_menu_confirm_submit($form, &$form_state) {
}
// Reset all the menu links defined by the system via hook_menu().
$result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $link) {
menu_reset_item($link);
// @todo Convert this to an EFQ once we figure out 'ORDER BY m.number_parts'.
$result = db_query("SELECT mlid FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC))->fetchCol();
$menu_links = menu_link_load_multiple($result);
foreach ($menu_links as $link) {
$link->reset();
}
// Delete all links to the overview page for this menu.
$result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = :link", array(':link' => 'admin/structure/menu/manage/' . $menu->id()), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $link) {
menu_link_delete($link['mlid']);
}
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu/manage/' . $menu->id()));
menu_link_delete_multiple(array_keys($menu_links));
// Delete the custom menu and all its menu links.
$menu->delete();
@ -561,7 +378,7 @@ function menu_edit_menu_name_exists($value) {
$custom_exists = entity_load('menu', $value);
// 'menu-' is added to the menu name to avoid name-space conflicts.
$value = 'menu-' . $value;
$link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $value))->fetchField();
$link_exists = entity_query('menu_link')->condition('menu_name', $value)->range(0,1)->count()->execute();
return $custom_exists || $link_exists;
}
@ -575,26 +392,23 @@ function menu_edit_menu_submit($form, &$form_state) {
if ($form['#insert']) {
// Add 'menu-' to the menu name to help avoid name-space conflicts.
$menu['id'] = 'menu-' . $menu['id'];
$link['link_title'] = $menu['label'];
$link['link_path'] = $path . $menu['id'];
$link['router_path'] = $path . '%';
$link['module'] = 'menu';
$link['plid'] = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :link AND module = :module", array(
':link' => 'admin/structure/menu',
':module' => 'system'
))
->fetchField();
menu_link_save($link);
$system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system'));
$system_link = reset($system_link);
$menu_link = entity_create('menu_link', array(
'link_title' => $menu['label'],
'link_path' => $path . $menu['id'],
'router_path' => $path . '%',
'plid' => $system_link->id(),
));
$menu_link->save();
menu_save($menu);
}
else {
menu_save($menu);
$result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path", array(':path' => $path . $menu['id']), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $m) {
$link = menu_link_load($m['mlid']);
$link['link_title'] = $menu['label'];
menu_link_save($link);
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path . $menu['id']));
foreach ($menu_links as $menu_link) {
$menu_link->link_title = $menu['label'];
$menu_link->save();
}
}
drupal_set_message(t('Your configuration has been saved.'));
@ -602,53 +416,80 @@ function menu_edit_menu_submit($form, &$form_state) {
}
/**
* Menu callback; Check access and present a confirm form for deleting a menu link.
* Menu callback: Provides the menu link submission form.
*
* @param \Drupal\system\Plugin\Core\Entity\Menu $menu
* An entity representing a custom menu.
*
* @return
* Returns the menu link submission form.
*/
function menu_item_delete_page($item) {
function menu_link_add(Menu $menu) {
$menu_link = entity_create('menu_link', array(
'mlid' => 0,
'plid' => 0,
'menu_name' => $menu->id(),
));
drupal_set_title(t('Add menu link'));
return entity_get_form($menu_link);
}
/**
* Menu callback; Check access and present a confirm form for deleting a menu
* link.
*/
function menu_link_delete_page(MenuLink $menu_link) {
// Links defined via hook_menu may not be deleted. Updated items are an
// exception, as they can be broken.
if ($item['module'] == 'system' && !$item['updated']) {
if ($menu_link->module == 'system' && !$menu_link->updated) {
throw new AccessDeniedHttpException();
}
return drupal_get_form('menu_item_delete_form', $item);
return drupal_get_form('menu_link_delete_form', $menu_link);
}
/**
* Build a confirm form for deletion of a single menu link.
*/
function menu_item_delete_form($form, &$form_state, $item) {
$form['#item'] = $item;
return confirm_form($form, t('Are you sure you want to delete the custom menu link %item?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name']);
function menu_link_delete_form($form, &$form_state, MenuLink $menu_link) {
$form['#menu_link'] = $menu_link;
return confirm_form($form,
t('Are you sure you want to delete the custom menu link %item?', array('%item' => $menu_link->link_title)),
'admin/structure/menu/manage/' . $menu_link->menu_name
);
}
/**
* Process menu delete form submissions.
* Processes menu link delete form submissions.
*/
function menu_item_delete_form_submit($form, &$form_state) {
$item = $form['#item'];
menu_link_delete($item['mlid']);
$t_args = array('%title' => $item['link_title']);
function menu_link_delete_form_submit($form, &$form_state) {
$menu_link = $form['#menu_link'];
menu_link_delete($menu_link->id());
$t_args = array('%title' => $menu_link->link_title);
drupal_set_message(t('The menu link %title has been deleted.', $t_args));
watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE);
$form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name'];
$form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name;
}
/**
* Menu callback; reset a single modified menu link.
* Menu callback; Reset a single modified menu link.
*/
function menu_reset_item_confirm($form, &$form_state, $item) {
$form['item'] = array('#type' => 'value', '#value' => $item);
return confirm_form($form, t('Are you sure you want to reset the link %item to its default values?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset'));
function menu_link_reset_form($form, &$form_state, MenuLink $menu_link) {
$form['#menu_link'] = $menu_link;
return confirm_form($form,
t('Are you sure you want to reset the link %item to its default values?', array('%item' => $menu_link->link_title)),
'admin/structure/menu/manage/' . $menu_link->menu_name,
t('Any customizations will be lost. This action cannot be undone.'),
t('Reset'));
}
/**
* Process menu reset item form submissions.
* Processes menu link reset form submissions.
*/
function menu_reset_item_confirm_submit($form, &$form_state) {
$item = $form_state['values']['item'];
$new_item = menu_reset_item($item);
function menu_link_reset_form_submit($form, &$form_state) {
$menu_link = $form['#menu_link'];
$new_menu_link = $menu_link->reset();
drupal_set_message(t('The menu link was reset to its default settings.'));
$form_state['redirect'] = 'admin/structure/menu/manage/' . $new_item['menu_name'];
$form_state['redirect'] = 'admin/structure/menu/manage/' . $new_menu_link->menu_name;
}
/**

View File

@ -3,4 +3,5 @@ description = Allows administrators to customize the site navigation menu.
package = Core
version = VERSION
core = 8.x
dependencies[] = menu_link
configure = admin/structure/menu

View File

@ -16,6 +16,8 @@ use Drupal\block\Plugin\Core\Entity\Block;
use Drupal\system\Plugin\Core\Entity\Menu;
use Drupal\system\Plugin\block\block\SystemMenuBlock;
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
use Drupal\menu_link\MenuLinkStorageController;
/**
* Maximum length of menu name as entered by the user. Database length is 32
@ -110,9 +112,9 @@ function menu_menu() {
// Not officially a local action, but displayed as such in
// menu_overview_form().
$items['admin/structure/menu/manage/%menu/add'] = array(
'title' => 'Add link',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_item', 'add', NULL, 4),
'title' => 'Add menu link',
'page callback' => 'menu_link_add',
'page arguments' => array(4),
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
);
@ -131,21 +133,20 @@ function menu_menu() {
);
$items['admin/structure/menu/item/%menu_link/edit'] = array(
'title' => 'Edit menu link',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
'page callback' => 'entity_get_form',
'page arguments' => array(4),
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
);
$items['admin/structure/menu/item/%menu_link/reset'] = array(
'title' => 'Reset menu link',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_reset_item_confirm', 4),
'page arguments' => array('menu_link_reset_form', 4),
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
);
$items['admin/structure/menu/item/%menu_link/delete'] = array(
'title' => 'Delete menu link',
'page callback' => 'menu_item_delete_page',
'page callback' => 'menu_link_delete_page',
'page arguments' => array(4),
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
@ -195,23 +196,29 @@ function menu_theme() {
*/
function menu_enable() {
menu_router_rebuild();
$base_link = db_query("SELECT mlid AS plid, menu_name FROM {menu_links} WHERE link_path = 'admin/structure/menu' AND module = 'system'")->fetchAssoc();
$base_link['router_path'] = 'admin/structure/menu/manage/%';
$base_link['module'] = 'menu';
$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/%',
'module' => 'menu',
));
$menus = entity_load_multiple('menu');
foreach ($menus as $menu) {
// $link is passed by reference to menu_link_save(), so we make a copy of $base_link.
$link = $base_link;
$link['mlid'] = 0;
$link['link_title'] = $menu->label();
$link['link_path'] = 'admin/structure/menu/manage/' . $menu->id();
$menu_link = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND plid = :plid", array(
':path' => $link['link_path'],
':plid' => $link['plid']
))
->fetchField();
if (!$menu_link) {
menu_link_save($link);
$link = $base_link->createDuplicate();
$link->plid = $system_link->id();
$link->link_title = $menu->label();
$link->link_path = 'admin/structure/menu/manage/' . $menu->id();
$query = entity_query('menu_link')
->condition('link_path', $link->link_path)
->condition('plid', $link->plid);
$result = $query->execute();
if (empty($result)) {
$link->save();
}
}
menu_cache_clear_all();
@ -293,24 +300,26 @@ function menu_menu_delete(Menu $menu) {
}
/**
* Return a list of menu items that are valid possible parents for the given menu item.
* Returns a list of menu links that are valid possible parents for the given
* menu link.
*
* @param $menus
* @param array $menus
* An array of menu names and titles, such as from menu_get_menus().
* @param $item
* The menu item or the node type for which to generate a list of parents.
* If $item['mlid'] == 0 then the complete tree is returned.
* @param $type
* @param \Drupal\menu_link\Plugin\Core\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
* An array of menu link titles keyed on the a string containing the menu name
* and mlid. The list excludes the given item and its children.
*
* @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($menus, $item, $type = '') {
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
@ -319,14 +328,12 @@ function menu_parent_options($menus, $item, $type = '') {
return array();
}
$available_menus = array();
if (!is_array($item)) {
// If $item is not an array then it is a node type.
// Use it as $type and prepare a dummy menu item for _menu_get_options().
$type = $item;
$item = array('mlid' => 0);
if (!$menu_link) {
$menu_link = entity_create('menu_link', array('mlid' => 0));
}
if (empty($type)) {
$available_menus = array();
if (!$type) {
// If no node type is set, use all menus given to this function.
$available_menus = $menus;
}
@ -338,7 +345,7 @@ function menu_parent_options($menus, $item, $type = '') {
}
}
return _menu_get_options($menus, $available_menus, $item);
return _menu_get_options($menus, $available_menus, $menu_link);
}
/**
@ -402,26 +409,6 @@ function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude,
}
}
/**
* Reset a system-defined menu link.
*/
function menu_reset_item($link) {
// To reset the link to its original values, we need to retrieve its
// definition from hook_menu(). Otherwise, for example, the link's menu would
// not be reset, because properties like the original 'menu_name' are not
// stored anywhere else. Since resetting a link happens rarely and this is a
// one-time operation, retrieving the full menu router does no harm.
$menu = menu_get_router();
$router_item = $menu[$link['router_path']];
$new_link = _menu_link_build($router_item);
// Merge existing menu link's ID and 'has_children' property.
foreach (array('mlid', 'has_children') as $key) {
$new_link[$key] = $link[$key];
}
menu_link_save($new_link);
return $new_link;
}
/**
* Implements hook_block_view_alter().
*/
@ -455,7 +442,7 @@ function menu_node_save(Node $node) {
if (isset($node->menu)) {
$link = &$node->menu;
if (empty($link['enabled'])) {
if (!empty($link['mlid'])) {
if (!$link->isNew()) {
menu_link_delete($link['mlid']);
}
}
@ -482,9 +469,13 @@ function menu_node_save(Node $node) {
*/
function menu_node_predelete(Node $node) {
// Delete all menu module links that point to this node.
$result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/' . $node->nid), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $m) {
menu_link_delete($m['mlid']);
$query = entity_query('menu_link')
->condition('link_path', 'node/' . $node->nid)
->condition('module', 'menu');
$result = $query->execute();
if (!empty($result)) {
menu_link_delete_multiple($result);
}
}
@ -495,42 +486,48 @@ function menu_node_prepare(Node $node) {
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->type, 'main:0'), ':');
$item = array();
$menu_link = FALSE;
if (isset($node->nid)) {
$mlid = FALSE;
// Give priority to the default menu
$type_menus = variable_get('menu_options_' . $node->type, array('main' => 'main'));
if (in_array($menu_name, $type_menus)) {
$mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", 0, 1, array(
':path' => 'node/' . $node->nid,
':menu_name' => $menu_name,
))->fetchField();
$query = entity_query('menu_link')
->condition('link_path', 'node/' . $node->nid)
->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)) {
$mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' AND menu_name IN (:type_menus) ORDER BY mlid ASC", 0, 1, array(
':path' => 'node/' . $node->nid,
':type_menus' => array_values($type_menus),
))->fetchField();
$query = entity_query('menu_link')
->condition('link_path', 'node/' . $node->nid)
->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) {
$item = menu_link_load($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 = $item + array(
'link_title' => '',
'mlid' => 0,
'plid' => 0,
'menu_name' => $menu_name,
'weight' => 0,
'options' => array(),
'module' => 'menu',
'expanded' => 0,
'hidden' => 0,
'has_children' => 0,
'customized' => 0,
);
$node->menu = $menu_link;
}
// Find the depth limit for the parent select.
if (!isset($node->menu['parent_depth_limit'])) {
@ -542,7 +539,7 @@ function menu_node_prepare(Node $node) {
* 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']) ? menu_link_children_relative_depth($item) : 0);
return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($item) : 0);
}
/**
@ -558,10 +555,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
$node = $form_state['controller']->getEntity($form_state);
$link = $node->menu;
$type = $node->type;
// menu_parent_options() is goofy and can actually handle either a menu link
// or a node type both as second argument. Pick based on whether there is
// a link already (menu_node_prepare() sets mlid default to 0).
$options = menu_parent_options(menu_get_menus(), $link['mlid'] ? $link : $type, $type);
$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;
@ -632,13 +626,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
);
// Get number of items in menu so the weight selector is sized appropriately.
$sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu";
$result = db_query($sql, array(':menu' => $link['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $row) {
foreach ($row as $menu_items) {
$delta = $menu_items;
}
}
$delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name);
if ($delta < 50) {
// Old hardcoded value
$delta = 50;
@ -658,6 +646,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
* @see menu_form_node_form_alter()
*/
function menu_node_submit(Node $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'])) {
@ -693,7 +682,8 @@ function menu_form_node_type_form_alter(&$form, $form_state) {
// 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.
$options = menu_parent_options(menu_get_menus(), array('mlid' => 0));
$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'),

View File

@ -0,0 +1,226 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkFormController.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityFormController;
/**
* Form controller for the node edit forms.
*/
class MenuLinkFormController extends EntityFormController {
/**
* Overrides EntityFormController::form().
*/
public function form(array $form, array &$form_state, EntityInterface $menu_link) {
// Since menu_link_load() no longer returns a translated and access checked
// item, do it here instead.
_menu_link_translate($menu_link);
if (!$menu_link->isNew()) {
// Get the human-readable menu title from the given menu name.
$titles = menu_get_menus();
$current_title = $titles[$menu_link->menu_name];
// Get the current breadcrumb and add a link to that menu's overview page.
$breadcrumb = menu_get_active_breadcrumb();
$breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $menu_link->menu_name);
drupal_set_breadcrumb($breadcrumb);
}
$form['link_title'] = array(
'#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $menu_link->link_title,
'#description' => t('The text to be used for this link in the menu.'),
'#required' => TRUE,
);
foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
$form[$key] = array('#type' => 'value', '#value' => $menu_link->{$key});
}
// Any item created or edited via this interface is considered "customized".
$form['customized'] = array('#type' => 'value', '#value' => 1);
// We are not using url() when constructing this path because it would add
// $base_path.
$path = $menu_link->link_path;
if (isset($menu_link->options['query'])) {
$path .= '?' . drupal_http_build_query($menu_link->options['query']);
}
if (isset($menu_link->options['fragment'])) {
$path .= '#' . $menu_link->options['fragment'];
}
if ($menu_link->module == 'menu') {
$form['link_path'] = array(
'#type' => 'textfield',
'#title' => t('Path'),
'#maxlength' => 255,
'#default_value' => $path,
'#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
'#required' => TRUE,
);
}
else {
$form['_path'] = array(
'#type' => 'item',
'#title' => t('Path'),
'#description' => l($menu_link->link_title, $menu_link->href, $menu_link->options),
);
}
$form['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => isset($menu_link->options['attributes']['title']) ? $menu_link->options['attributes']['title'] : '',
'#rows' => 1,
'#description' => t('Shown when hovering over the menu link.'),
);
$form['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enabled'),
'#default_value' => !$menu_link->hidden,
'#description' => t('Menu links that are not enabled will not be listed in any menu.'),
);
$form['expanded'] = array(
'#type' => 'checkbox',
'#title' => t('Show as expanded'),
'#default_value' => $menu_link->expanded,
'#description' => t('If selected and this menu link has children, the menu will always appear expanded.'),
);
// Generate a list of possible parents (not including this link or descendants).
$options = menu_parent_options(menu_get_menus(), $menu_link);
$default = $menu_link->menu_name . ':' . $menu_link->plid;
if (!isset($options[$default])) {
$default = 'tools:0';
}
$form['parent'] = array(
'#type' => 'select',
'#title' => t('Parent link'),
'#default_value' => $default,
'#options' => $options,
'#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#attributes' => array('class' => array('menu-title-select')),
);
// Get number of items in menu so the weight selector is sized appropriately.
$delta = drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link')->countMenuLinks($menu_link->menu_name);
$form['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
// Old hardcoded value.
'#delta' => max($delta, 50),
'#default_value' => $menu_link->weight,
'#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
);
$form['langcode'] = array(
'#type' => 'language_select',
'#title' => t('Language'),
'#languages' => LANGUAGE_ALL,
'#default_value' => $menu_link->langcode,
);
return parent::form($form, $form_state, $menu_link);
}
/**
* Overrides EntityFormController::actions().
*/
protected function actions(array $form, array &$form_state) {
$element = parent::actions($form, $form_state);
$element['submit']['#button_type'] = 'primary';
$element['delete']['#access'] = $this->getEntity($form_state)->module == 'menu';
return $element;
}
/**
* Overrides EntityFormController::validate().
*/
public function validate(array $form, array &$form_state) {
$menu_link = $this->buildEntity($form, $form_state);
$normal_path = drupal_container()->get('path.alias_manager.cached')->getSystemPath($menu_link->link_path);
if ($menu_link->link_path != $normal_path) {
drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $menu_link->link_path, '%normal_path' => $normal_path)));
$menu_link->link_path = $normal_path;
}
if (!url_is_external($menu_link->link_path)) {
$parsed_link = parse_url($menu_link->link_path);
if (isset($parsed_link['query'])) {
$menu_link->options['query'] = drupal_get_query_array($parsed_link['query']);
}
else {
// Use unset() rather than setting to empty string
// to avoid redundant serialized data being stored.
unset($menu_link->options['query']);
}
if (isset($parsed_link['fragment'])) {
$menu_link->options['fragment'] = $parsed_link['fragment'];
}
else {
unset($menu_link->options['fragment']);
}
if (isset($parsed_link['path']) && $menu_link->link_path != $parsed_link['path']) {
$menu_link->link_path = $parsed_link['path'];
}
}
if (!trim($menu_link->link_path) || !drupal_valid_path($menu_link->link_path, TRUE)) {
form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $menu_link->link_path)));
}
parent::validate($form, $form_state);
}
/**
* Overrides EntityFormController::submit().
*/
public function submit(array $form, array &$form_state) {
// Build the menu link object from the submitted values.
$menu_link = parent::submit($form, $form_state);
// The value of "hidden" is the opposite of the value supplied by the
// "enabled" checkbox.
$menu_link->hidden = (int) !$menu_link->enabled;
unset($menu_link->enabled);
$menu_link->options['attributes']['title'] = $menu_link->description;
list($menu_link->menu_name, $menu_link->plid) = explode(':', $menu_link->parent);
return $menu_link;
}
/**
* Overrides EntityFormController::save().
*/
public function save(array $form, array &$form_state) {
$menu_link = $this->getEntity($form_state);
$saved = $menu_link->save();
if ($saved) {
drupal_set_message(t('The menu link has been saved.'));
$form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name;
}
else {
drupal_set_message(t('There was an error saving the menu link.'), 'error');
$form_state['rebuild'] = TRUE;
}
}
/**
* Overrides EntityFormController::delete().
*/
public function delete(array $form, array &$form_state) {
$menu_link = $this->getEntity($form_state);
$form_state['redirect'] = 'admin/structure/menu/item/' . $menu_link->id() . '/delete';
}
}

View File

@ -0,0 +1,564 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkStorageController.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
/**
* Controller class for menu links.
*
* This extends the Drupal\entity\DatabaseStorageController class, adding
* required special handling for menu_link entities.
*/
class MenuLinkStorageController extends DatabaseStorageController {
/**
* Indicates whether the delete operation should re-parent children items.
*
* @var bool
*/
protected $preventReparenting = FALSE;
/**
* Holds an array of router item schema fields.
*
* @var array
*/
protected static $routerItemFields = array();
/**
* Overrides DatabaseStorageController::__construct().
*/
public function __construct($entityType) {
parent::__construct($entityType);
if (empty(static::$routerItemFields)) {
static::$routerItemFields = array_diff(drupal_schema_fields_sql('menu_router'), array('weight'));
}
}
/**
* Overrides DatabaseStorageController::buildQuery().
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = parent::buildQuery($ids, $revision_id);
// Specify additional fields from the {menu_router} table.
$query->leftJoin('menu_router', 'm', 'base.router_path = m.path');
$query->fields('m', static::$routerItemFields);
return $query;
}
/**
* Overrides DatabaseStorageController::attachLoad().
*
* @todo Don't call parent::attachLoad() at all because we want to be able to
* control the entity load hooks.
*/
protected function attachLoad(&$menu_links, $load_revision = FALSE) {
foreach ($menu_links as &$menu_link) {
$menu_link->options = unserialize($menu_link->options);
// Use the weight property from the menu link.
$menu_link->router_item['weight'] = $menu_link->weight;
}
parent::attachLoad($menu_links, $load_revision);
}
/**
* Overrides DatabaseStorageController::save().
*/
public function save(EntityInterface $entity) {
// We return SAVED_UPDATED by default because the logic below might not
// update the entity if its values haven't changed, so returning FALSE
// would be confusing in that situation.
$return = SAVED_UPDATED;
$transaction = db_transaction();
try {
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
}
if ($entity->isNew()) {
$entity->mlid = db_insert($this->entityInfo['base_table'])->fields(array('menu_name' => 'tools'))->execute();
$entity->enforceIsNew();
}
// Unlike the save() method from DatabaseStorageController, we invoke the
// 'presave' hook first because we want to allow modules to alter the
// entity before all the logic from our preSave() method.
$this->invokeHook('presave', $entity);
$this->preSave($entity);
// If every value in $entity->original is the same in the $entity, there
// is no reason to run the update queries or clear the caches. We use
// array_intersect_key() with the $entity as the first parameter because
// $entity may have additional keys left over from building a router entry.
// The intersect removes the extra keys, allowing a meaningful comparison.
if ($entity->isNew() || (array_intersect_key(get_object_vars($entity), get_object_vars($entity->original)) != get_object_vars($entity->original))) {
$return = drupal_write_record($this->entityInfo['base_table'], $entity, $this->idKey);
if ($return) {
if (!$entity->isNew()) {
$this->resetCache(array($entity->{$this->idKey}));
$this->postSave($entity, TRUE);
$this->invokeHook('update', $entity);
}
else {
$return = SAVED_NEW;
$this->resetCache();
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$this->invokeHook('insert', $entity);
}
}
}
// Ignore slave server temporarily.
db_ignore_slave();
unset($entity->original);
return $return;
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Overrides DatabaseStorageController::preSave().
*/
protected function preSave(EntityInterface $entity) {
// This is the easiest way to handle the unique internal path '<front>',
// since a path marked as external does not need to match a router path.
$entity->external = (url_is_external($entity->link_path) || $entity->link_path == '<front>') ? 1 : 0;
// Try to find a parent link. If found, assign it and derive its menu.
$parent_candidates = !empty($entity->parentCandidates) ? $entity->parentCandidates : array();
$parent = $this->findParent($entity, $parent_candidates);
if ($parent) {
$entity->plid = $parent->id();
$entity->menu_name = $parent->menu_name;
}
// If no corresponding parent link was found, move the link to the top-level.
else {
$entity->plid = 0;
}
// Directly fill parents for top-level links.
if ($entity->plid == 0) {
$entity->p1 = $entity->id();
for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
$parent_property = "p$i";
$entity->$parent_property = 0;
}
$entity->depth = 1;
}
// Otherwise, ensure that this link's depth is not beyond the maximum depth
// and fill parents based on the parent link.
else {
if ($entity->has_children && $entity->original) {
$limit = MENU_MAX_DEPTH - $this->findChildrenRelativeDepth($entity->original) - 1;
}
else {
$limit = MENU_MAX_DEPTH - 1;
}
if ($parent->depth > $limit) {
return FALSE;
}
$entity->depth = $parent->depth + 1;
$this->setParents($entity, $parent);
}
// Need to check both plid and menu_name, since plid can be 0 in any menu.
if (isset($entity->original) && ($entity->plid != $entity->original->plid || $entity->menu_name != $entity->original->menu_name)) {
$this->moveChildren($entity, $entity->original);
}
// Find the router_path.
if (empty($entity->router_path) || empty($entity->original) || (isset($entity->original) && $entity->original->link_path != $entity->link_path)) {
if ($entity->external) {
$entity->router_path = '';
}
else {
// Find the router path which will serve this path.
$entity->parts = explode('/', $entity->link_path, MENU_MAX_PARTS);
$entity->router_path = _menu_find_router_path($entity->link_path);
}
}
}
/**
* DatabaseStorageController::postSave().
*/
function postSave(EntityInterface $entity, $update) {
// Check the has_children status of the parent.
$this->updateParentalStatus($entity);
menu_cache_clear($entity->menu_name);
if (isset($entity->original) && $entity->menu_name != $entity->original->menu_name) {
menu_cache_clear($entity->original->menu_name);
}
// Now clear the cache.
_menu_clear_page_cache();
}
/**
* Sets an internal flag that allows us to prevent the reparenting operations
* executed during deletion.
*
* @param bool $value
*/
public function preventReparenting($value = FALSE) {
$this->preventReparenting = $value;
}
/**
* Overrides DatabaseStorageController::preDelete().
*/
protected function preDelete($entities) {
// Nothing to do if we don't want to reparent children.
if ($this->preventReparenting) {
return;
}
foreach ($entities as $entity) {
// Children get re-attached to the item's parent.
if ($entity->has_children) {
$children = $this->loadByProperties(array('plid' => $entity->plid));
foreach ($children as $child) {
$child->plid = $entity->plid;
$this->save($child);
}
}
}
}
/**
* Overrides DatabaseStorageController::postDelete().
*/
protected function postDelete($entities) {
$affected_menus = array();
// Update the has_children status of the parent.
foreach ($entities as $entity) {
if (!$this->preventReparenting) {
$this->updateParentalStatus($entity);
}
// Store all menu names for which we need to clear the cache.
if (!isset($affected_menus[$entity->menu_name])) {
$affected_menus[$entity->menu_name] = $entity->menu_name;
}
}
foreach ($affected_menus as $menu_name) {
menu_cache_clear($menu_name);
}
_menu_clear_page_cache();
}
/**
* Loads updated and customized menu links for specific router paths.
*
* Note that this is a low-level method and it doesn't return fully populated
* menu link entities. (e.g. no fields are attached)
*
* @param array $router_paths
* An array of router paths.
*
* @return array
* An array of menu link objects indexed by their ids.
*/
public function loadUpdatedCustomized(array $router_paths) {
$query = parent::buildQuery(NULL);
$query
->condition(db_or()
->condition('updated', 1)
->condition(db_and()
->condition('router_path', $router_paths, 'NOT IN')
->condition('external', 0)
->condition('customized', 1)
)
);
$query_result = $query->execute();
if (!empty($this->entityInfo['class'])) {
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @see Drupal\Core\Entity\EntityInterface::__construct()
$query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityInfo['class'], array(array(), $this->entityType));
}
return $query_result->fetchAllAssoc($this->idKey);
}
/**
* Loads system menu link as needed by system_get_module_admin_tasks().
*
* @return array
* An array of menu link entities indexed by their IDs.
*/
public function loadModuleAdminTasks() {
$query = $this->buildQuery(NULL);
$query
->condition('base.link_path', 'admin/%', 'LIKE')
->condition('base.hidden', 0, '>=')
->condition('base.module', 'system')
->condition('m.number_parts', 1, '>')
->condition('m.page_callback', 'system_admin_menu_block_page', '<>');
$ids = $query->execute()->fetchCol(1);
return $this->load($ids);
}
/**
* Checks and updates the 'has_children' property for the parent of a link.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
protected function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
// If plid == 0, there is nothing to update.
if ($entity->plid && ($parent_entity = $this->load(array($entity->plid)))) {
// Check if at least one visible child exists in the table.
$query = entity_query($this->entityType);
$query
->condition('menu_name', $entity->menu_name)
->condition('hidden', 0)
->condition('plid', $entity->plid)
->count();
if ($exclude) {
$query->condition('mlid', $entity->id(), '<>');
}
$parent_has_children = ((bool) $query->execute()) ? 1 : 0;
$parent_entity = reset($parent_entity);
$parent_entity->has_children = $parent_has_children;
$parent_entity->save();
}
}
/**
* Finds a possible parent for a given menu link entity.
*
* Because the parent of a given link might not exist anymore in the database,
* we apply a set of heuristics to determine a proper parent:
*
* - use the passed parent link if specified and existing.
* - else, use the first existing link down the previous link hierarchy
* - else, for system menu links (derived from hook_menu()), reparent
* based on the path hierarchy.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* @param array $parent_candidates
* An array of menu link entities keyed by mlid.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* A menu link entity structure of the possible parent or FALSE if no valid
* parent has been found.
*/
protected function findParent(EntityInterface $entity, array $parent_candidates = array()) {
$parent = FALSE;
// This item is explicitely top-level, skip the rest of the parenting.
if (isset($entity->plid) && empty($entity->plid)) {
return $parent;
}
// If we have a parent link ID, try to use that.
$candidates = array();
if (isset($entity->plid)) {
$candidates[] = $entity->plid;
}
// Else, if we have a link hierarchy try to find a valid parent in there.
if (!empty($entity->depth) && $entity->depth > 1) {
for ($depth = $entity->depth - 1; $depth >= 1; $depth--) {
$parent_property = "p$depth";
$candidates[] = $entity->$parent_property;
}
}
foreach ($candidates as $mlid) {
if (isset($parent_candidates[$mlid])) {
$parent = $parent_candidates[$mlid];
}
else {
$parent = $this->load(array($mlid));
$parent = reset($parent);
}
if ($parent) {
return $parent;
}
}
// If everything else failed, try to derive the parent from the path
// hierarchy. This only makes sense for links derived from menu router
// items (ie. from hook_menu()).
if ($entity->module == 'system') {
// Find the parent - it must be unique.
$parent_path = $entity->link_path;
do {
$parent = FALSE;
$parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
// @todo Return to the previous method of cloning the entity query when
// http://drupal.org/node/1829942 is fixed.
$query = entity_query($this->entityType);
$query
->condition('mlid', $entity->id(), '<>')
->condition('module', 'system')
// We always respect the link's 'menu_name'; inheritance for router
// items is ensured in _menu_router_build().
->condition('menu_name', $entity->menu_name)
->condition('link_path', $parent_path);
$count_query = clone $query;
// Only valid if we get a unique result.
if ($count_query->count()->execute() == 1) {
$result = $query->execute();
$parent = $this->load($result);
$parent = reset($parent);
}
} while ($parent === FALSE && $parent_path);
}
return $parent;
}
/**
* Sets the p1 through p9 properties for a menu link entity being saved.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
* @param \Drupal\Core\Entity\EntityInterface $parent
* A menu link entity.
*/
protected function setParents(EntityInterface $entity, EntityInterface $parent) {
$i = 1;
while ($i < $entity->depth) {
$p = 'p' . $i++;
$entity->{$p} = $parent->{$p};
}
$p = 'p' . $i++;
// The parent (p1 - p9) corresponding to the depth always equals the mlid.
$entity->{$p} = $entity->id();
while ($i <= MENU_MAX_DEPTH) {
$p = 'p' . $i++;
$entity->{$p} = 0;
}
}
/**
* Finds the depth of an item's children relative to its depth.
*
* For example, if the item has a depth of 2 and the maximum of any child in
* the menu link tree is 5, the relative depth is 3.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return int
* The relative depth, or zero.
*/
public function findChildrenRelativeDepth(EntityInterface $entity) {
// @todo Since all we need is a specific field from the base table, does it
// make sense to convert to EFQ?
$query = db_select('menu_links');
$query->addField('menu_links', 'depth');
$query->condition('menu_name', $entity->menu_name);
$query->orderBy('depth', 'DESC');
$query->range(0, 1);
$i = 1;
$p = 'p1';
while ($i <= MENU_MAX_DEPTH && $entity->{$p}) {
$query->condition($p, $entity->{$p});
$p = 'p' . ++$i;
}
$max_depth = $query->execute()->fetchField();
return ($max_depth > $entity->depth) ? $max_depth - $entity->depth : 0;
}
/**
* Updates the children of a menu link that is being moved.
*
* The menu name, parents (p1 - p6), and depth are updated for all children of
* the link, and the has_children status of the previous parent is updated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
protected function moveChildren(EntityInterface $entity) {
$query = db_update($this->entityInfo['base_table']);
$query->fields(array('menu_name' => $entity->menu_name));
$p = 'p1';
$expressions = array();
for ($i = 1; $i <= $entity->depth; $p = 'p' . ++$i) {
$expressions[] = array($p, ":p_$i", array(":p_$i" => $entity->{$p}));
}
$j = $entity->original->depth + 1;
while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 'p' . $j++, array());
}
while ($i <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 0, array());
}
$shift = $entity->depth - $entity->original->depth;
if ($shift > 0) {
// The order of expressions must be reversed so the new values don't
// overwrite the old ones before they can be used because "Single-table
// UPDATE assignments are generally evaluated from left to right"
// @see http://dev.mysql.com/doc/refman/5.0/en/update.html
$expressions = array_reverse($expressions);
}
foreach ($expressions as $expression) {
$query->expression($expression[0], $expression[1], $expression[2]);
}
$query->expression('depth', 'depth + :depth', array(':depth' => $shift));
$query->condition('menu_name', $entity->original->menu_name);
$p = 'p1';
for ($i = 1; $i <= MENU_MAX_DEPTH && $entity->original->{$p}; $p = 'p' . ++$i) {
$query->condition($p, $entity->original->{$p});
}
$query->execute();
// Check the has_children status of the parent, while excluding this item.
$this->updateParentalStatus($entity->original, TRUE);
}
/**
* Returns the number of menu links from a menu.
*
* @param string $menu_name
* The unique name of a menu.
*/
public function countMenuLinks($menu_name) {
$query = entity_query($this->entityType);
$query
->condition('menu_name', $menu_name)
->count();
return $query->execute();
}
}

View File

@ -0,0 +1,330 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\Plugin\Core\Entity\MenuLink.
*/
namespace Drupal\menu_link\Plugin\Core\Entity;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity;
/**
* Defines the menu link entity class.
*
* @Plugin(
* id = "menu_link",
* label = @Translation("Menu link"),
* module = "menu_link",
* controller_class = "Drupal\menu_link\MenuLinkStorageController",
* form_controller_class = {
* "default" = "Drupal\menu_link\MenuLinkFormController"
* },
* static_cache = FALSE,
* base_table = "menu_links",
* uri_callback = "menu_link_uri",
* entity_keys = {
* "id" = "mlid",
* "label" = "link_title",
* "uuid" = "uuid"
* },
* bundles = {
* "menu_link" = {
* "label" = "Menu link",
* }
* }
* )
*/
class MenuLink extends Entity implements \ArrayAccess, ContentEntityInterface {
/**
* The link's menu name.
*
* @var string
*/
public $menu_name = 'tools';
/**
* The menu link ID.
*
* @var int
*/
public $mlid;
/**
* The menu link UUID.
*
* @var string
*/
public $uuid;
/**
* The parent link ID.
*
* @var int
*/
public $plid;
/**
* The Drupal path or external path this link points to.
*
* @var string
*/
public $link_path;
/**
* For links corresponding to a Drupal path (external = 0), this connects the
* link to a {menu_router}.path for joins.
*
* @var string
*/
public $router_path;
/**
* The entity label.
*
* @var string
*/
public $link_title = '';
/**
* A serialized array of options to be passed to the url() or l() function,
* such as a query string or HTML attributes.
*
* @var array
*/
public $options = array();
/**
* The name of the module that generated this link.
*
* @var string
*/
public $module = 'menu';
/**
* A flag for whether the link should be rendered in menus.
*
* @var int
*/
public $hidden = 0;
/**
* A flag to indicate if the link points to a full URL starting with a
* protocol, like http:// (1 = external, 0 = internal).
*
* @var int
*/
public $external;
/**
* Flag indicating whether any links have this link as a parent.
*
* @var int
*/
public $has_children = 0;
/**
* Flag for whether this link should be rendered as expanded in menus.
* Expanded links always have their child links displayed, instead of only
* when the link is in the active trail.
*
* @var int
*/
public $expanded = 0;
/**
* Link weight among links in the same menu at the same depth.
*
* @var int
*/
public $weight = 0;
/**
* The depth relative to the top level. A link with plid == 0 will have
* depth == 1.
*
* @var int
*/
public $depth;
/**
* A flag to indicate that the user has manually created or edited the link.
*
* @var int
*/
public $customized = 0;
/**
* The first entity ID in the materialized path.
*
* @var int
*
* @todo Investigate whether the p1, p2, .. pX properties can be moved to a
* single array property.
*/
public $p1;
/**
* The second entity ID in the materialized path.
*
* @var int
*/
public $p2;
/**
* The third entity ID in the materialized path.
*
* @var int
*/
public $p3;
/**
* The fourth entity ID in the materialized path.
*
* @var int
*/
public $p4;
/**
* The fifth entity ID in the materialized path.
*
* @var int
*/
public $p5;
/**
* The sixth entity ID in the materialized path.
*
* @var int
*/
public $p6;
/**
* The seventh entity ID in the materialized path.
*
* @var int
*/
public $p7;
/**
* The eighth entity ID in the materialized path.
*
* @var int
*/
public $p8;
/**
* The ninth entity ID in the materialized path.
*
* @var int
*/
public $p9;
/**
* The menu link modification timestamp.
*
* @var int
*/
public $updated = 0;
/**
* Overrides Entity::id().
*/
public function id() {
return $this->mlid;
}
/**
* Overrides Entity::createDuplicate().
*/
public function createDuplicate() {
$duplicate = parent::createDuplicate();
$duplicate->plid = NULL;
return $duplicate;
}
/**
* Resets a system-defined menu link.
*
* @return \Drupal\Core\Entity\EntityInterface
* A menu link entity.
*/
public function reset() {
// To reset the link to its original values, we need to retrieve its
// definition from hook_menu(). Otherwise, for example, the link's menu
// would not be reset, because properties like the original 'menu_name' are
// not stored anywhere else. Since resetting a link happens rarely and this
// is a one-time operation, retrieving the full menu router does no harm.
$menu = menu_get_router();
$router_item = $menu[$this->router_path];
$new_link = self::buildFromRouterItem($router_item);
// Merge existing menu link's ID and 'has_children' property.
foreach (array('mlid', 'has_children') as $key) {
$new_link->{$key} = $this->{$key};
}
$new_link->save();
return $new_link;
}
/**
* Builds a menu link entity from a router item.
*
* @param array $item
* A menu router item.
*
* @return MenuLink
* A menu link entity.
*/
public static function buildFromRouterItem(array $item) {
// Suggested items are disabled by default.
if ($item['type'] == MENU_SUGGESTED_ITEM) {
$item['hidden'] = 1;
}
// Hide all items that are not visible in the tree.
elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
$item['hidden'] = -1;
}
// Note, we set this as 'system', so that we can be sure to distinguish all
// the menu links generated automatically from entries in {menu_router}.
$item['module'] = 'system';
$item += array(
'link_title' => $item['title'],
'link_path' => $item['path'],
'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
);
return drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link')->create($item);
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->{$offset});
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function &offsetGet($offset) {
return $this->{$offset};
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
$this->{$offset} = $value;
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->{$offset});
}
}

View File

@ -0,0 +1,138 @@
<?php
/**
* @file
* Hooks provided by the Menu link module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter menu links when loaded and before they are rendered.
*
* This hook is only invoked if $menu_link->options['alter'] has been set to a
* non-empty value (e.g., TRUE). This flag should be set using
* hook_menu_link_presave().
* @ todo The paragraph above is lying! This hasn't been (re)implemented yet.
*
* Implementations of this hook are able to alter any property of the menu link.
* For example, this hook may be used to add a page-specific query string to all
* menu links, or hide a certain link by setting:
* @code
* 'hidden' => 1,
* @endcode
*
* @param array $menu_links
* An array of menu link entities.
*
* @see hook_menu_link_presave()
*/
function hook_menu_link_load($menu_links) {
foreach ($menu_links as $menu_link) {
if ($menu_link->href == 'devel/cache/clear') {
$menu_link->options['query'] = drupal_get_destination();
}
}
}
/**
* Alter the data of a menu link entity before it is created or updated.
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*
* @see hook_menu_link_load()
*/
function hook_menu_link_presave($menu_link) {
// Make all new admin links hidden (a.k.a disabled).
if (strpos($menu_link->link_path, 'admin') === 0 && $menu_link->isNew()) {
$menu_link->hidden = 1;
}
// Flag a link to be altered by hook_menu_link_load().
if ($menu_link->link_path == 'devel/cache/clear') {
$menu_link->options['alter'] = TRUE;
}
// Flag a menu link to be altered by hook_menu_link_load(), but only if it is
// derived from a menu router item; i.e., do not alter a custom menu link
// pointing to the same path that has been created by a user.
if ($menu_link->link_path == 'user' && $menu_link->module == 'system') {
$menu_link->options['alter'] = TRUE;
}
}
/**
* Inform modules that a menu link has been created.
*
* This hook is used to notify modules that menu links have been
* created. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*
* @see hook_menu_link_presave()
* @see hook_menu_link_update()
* @see hook_menu_link_delete()
*/
function hook_menu_link_insert($menu_link) {
// In our sample case, we track menu items as editing sections
// of the site. These are stored in our table as 'disabled' items.
$record['mlid'] = $menu_link->id();
$record['menu_name'] = $menu_link->menu_name;
$record['status'] = 0;
drupal_write_record('menu_example', $record);
}
/**
* Inform modules that a menu link has been updated.
*
* This hook is used to notify modules that menu items have been
* updated. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*
* @see hook_menu_link_presave()
* @see hook_menu_link_insert()
* @see hook_menu_link_delete()
*/
function hook_menu_link_update($menu_link) {
// If the parent menu has changed, update our record.
$menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $menu_link->id()))->fetchField();
if ($menu_name != $menu_link->menu_name) {
db_update('menu_example')
->fields(array('menu_name' => $menu_link->menu_name))
->condition('mlid', $menu_link->id())
->execute();
}
}
/**
* Inform modules that a menu link has been deleted.
*
* This hook is used to notify modules that menu links have been
* deleted. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*
* @see hook_menu_link_presave()
* @see hook_menu_link_insert()
* @see hook_menu_link_update()
*/
function hook_menu_link_delete($menu_link) {
// Delete the record from our table.
db_delete('menu_example')
->condition('mlid', $menu_link->id())
->execute();
}
/**
* @} End of "addtogroup hooks".
*/

View File

@ -0,0 +1,8 @@
name = Menu Link
description = Provides menu links, trees and bunnies!
package = Core
version = VERSION
core = 8.x
; @todo Menu links functionality has been moved from system.module and menu.inc
; to this module, so make it required until everything is moved over.
required = TRUE

View File

@ -0,0 +1,213 @@
<?php
/**
* @file
* Install, update and uninstall functions for the menu_link module.
*/
/**
* Implements hook_schema().
*/
function menu_link_schema() {
$schema['menu_links'] = array(
'description' => 'Contains the individual links within a menu.',
'fields' => array(
'menu_name' => array(
'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'mlid' => array(
'description' => 'The menu link ID (mlid) is the integer primary key.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'uuid' => array(
'description' => 'Unique Key: Universally unique identifier for this entity.',
'type' => 'varchar',
'length' => 128,
'not null' => FALSE,
),
'plid' => array(
'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'link_path' => array(
'description' => 'The Drupal path or external path this link points to.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'router_path' => array(
'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'langcode' => array(
'description' => 'The {language}.langcode of this link.',
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'link_title' => array(
'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'translatable' => TRUE,
),
'options' => array(
'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
'type' => 'blob',
'not null' => FALSE,
'translatable' => TRUE,
'serialize' => TRUE,
),
'module' => array(
'description' => 'The name of the module that generated this link.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'system',
),
'hidden' => array(
'description' => 'A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'external' => array(
'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'has_children' => array(
'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'expanded' => array(
'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'weight' => array(
'description' => 'Link weight among links in the same menu at the same depth.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'depth' => array(
'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'customized' => array(
'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'p1' => array(
'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the plid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p2' => array(
'description' => 'The second mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p3' => array(
'description' => 'The third mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p4' => array(
'description' => 'The fourth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p5' => array(
'description' => 'The fifth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p6' => array(
'description' => 'The sixth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p7' => array(
'description' => 'The seventh mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p8' => array(
'description' => 'The eighth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p9' => array(
'description' => 'The ninth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'updated' => array(
'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
),
'indexes' => array(
'path_menu' => array(array('link_path', 128), 'menu_name'),
'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'),
'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
'router_path' => array(array('router_path', 128)),
),
'primary key' => array('mlid'),
);
return $schema;
}

View File

@ -0,0 +1,183 @@
<?php
/**
* @file
* Enables users to create menu links.
*/
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
/**
* Entity URI callback.
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* A menu link entity.
*/
function menu_link_uri(MenuLink $menu_link) {
return array(
'path' => $menu_link->link_path,
);
}
/**
* Loads a menu link entity.
*
* This function should never be called from within node_load() or any other
* function used as a menu object load function since an infinite recursion may
* occur.
*
* @param int $mlid
* The menu link ID.
* @param bool $reset
* (optional) Whether to reset the menu_link_load_multiple() cache.
*
* @return \Drupal\menu_link\Plugin\Core\Entity\MenuLink|false
* A menu link entity, or FALSE if there is no entity with the given ID.
*/
function menu_link_load($mlid = NULL, $reset = FALSE) {
return entity_load('menu_link', $mlid, $reset);
}
/**
* Loads menu link entities from the database.
*
* @param array $mlids
* (optional) An array of entity IDs. If omitted, all entities are loaded.
* @param bool $reset
* (optional) Whether to reset the internal cache.
*
* @return array<\Drupal\menu_link\Plugin\Core\Entity\MenuLink>
* An array of menu link entities indexed by entity IDs.
*
* @see menu_link_load()
* @see entity_load_multiple()
*/
function menu_link_load_multiple(array $mlids = NULL, $reset = FALSE) {
return entity_load_multiple('menu_link', $mlids, $reset);
}
/**
* Deletes a menu link.
*
* @param int $mlid
* The menu link ID.
*
* @see menu_link_delete_multiple()
*/
function menu_link_delete($mlid) {
menu_link_delete_multiple(array($mlid));
}
/**
* Deletes multiple menu links.
*
* @param array $mlids
* An array of menu link IDs.
* @param bool $force
* (optional) Forces deletion. Internal use only, setting to TRUE is
* discouraged. Defaults to FALSE.
* @param bool $prevent_reparenting
* (optional) Disables the re-parenting logic from the deletion process.
* Defaults to FALSE.
*/
function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_reparenting = FALSE) {
if (!$mlids) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$controller = drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link');
if (!$force) {
$entity_query = entity_query('menu_link');
$group = $entity_query->orConditionGroup()
->condition('module', 'system', '<>')
->condition('updated', 0, '<>');
$entity_query->condition('mlid', $mlids, 'IN');
$entity_query->condition($group);
$result = $entity_query->execute();
$entities = $controller->load($result);
}
else {
$entities = $controller->load($mlids);
}
$controller->preventReparenting($prevent_reparenting);
$controller->delete($entities);
}
/**
* Saves a menu link.
*
* After calling this function, rebuild the menu cache using
* menu_cache_clear_all().
*
* @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
* The menu link entity to be saved.
*/
function menu_link_save(MenuLink $menu_link) {
$menu_link->save();
}
/**
* Inserts, updates, enables, disables, or deletes an uncustomized menu link.
*
* @param string $module
* The name of the module that owns the link.
* @param string $op
* Operation to perform: insert, update, enable, disable, or delete.
* @param string $link_path
* The path this link points to.
* @param string $link_title
* (optional) Title of the link to insert or new title to update the link to.
* Unused for delete. Defaults to NULL.
*
* @return integer|null
* The insert op returns the mlid of the new item. Others op return NULL.
*/
function menu_link_maintain($module, $op, $link_path, $link_title = NULL) {
$menu_link_controller = drupal_container()->get('plugin.manager.entity')
->getStorageController('menu_link');
switch ($op) {
case 'insert':
$menu_link = entity_create('menu_link', array(
'link_title' => $link_title,
'link_path' => $link_path,
'module' => $module,)
);
return $menu_link->save();
case 'update':
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
foreach ($menu_links as $menu_link) {
$menu_link->original = clone $menu_link;
if (isset($link_title)) {
$menu_link->link_title = $link_title;
}
$menu_link_controller->save($menu_link);
}
break;
case 'enable':
case 'disable':
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
foreach ($menu_links as $menu_link) {
$menu_link->original = clone $menu_link;
$menu_link->hidden = ($op == 'disable' ? 1 : 0);
$menu_link->customized = 1;
if (isset($link_title)) {
$menu_link->link_title = $link_title;
}
$menu_link_controller->save($menu_link);
}
break;
case 'delete':
$result = entity_query('menu_link')->condition('link_path', $link_path)->execute();
if (!empty($result)) {
menu_link_delete_multiple($result);
}
break;
}
}

View File

@ -15,6 +15,21 @@ use Drupal\Core\Entity\EntityInterface;
*/
class ShortcutStorageController extends ConfigStorageController {
/**
* Overrides \Drupal\config\ConfigStorageController::attachLoad().
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
parent::attachLoad($queried_entities, $revision_id);
foreach ($queried_entities as $id => $entity) {
$links = menu_load_links('shortcut-' . $id);
foreach ($links as $menu_link) {
$entity->links[$menu_link->uuid()] = $menu_link;
}
}
}
/**
* Overrides \Drupal\config\ConfigStorageController::save().
*/
@ -27,8 +42,22 @@ class ShortcutStorageController extends ConfigStorageController {
// Size of menu_name is 32 so id could be 23 = 32 - strlen('shortcut-').
$id = substr($entity->id(), 0, 23);
$entity->set('id', $id);
$entity->set('links', menu_links_clone($default_set->links, $id));
$entity->set('links', $default_set->links);
foreach ($entity->links as $link) {
$link = $link->createDuplicate();
$link->menu_name = $id;
unset($link->mlid);
$link->save();
}
}
// Just store the UUIDs.
if (isset($entity->links)) {
foreach ($entity->links as $uuid => $link) {
$entity->links[$uuid] = $uuid;
}
}
return parent::save($entity);
}
@ -39,13 +68,14 @@ class ShortcutStorageController extends ConfigStorageController {
// Process links in shortcut set.
// If links were provided for the set, save them.
if (isset($entity->links)) {
foreach ($entity->links as &$link) {
foreach ($entity->links as $uuid) {
$menu_link = entity_load_by_uuid('menu_link', $uuid);
// Do not specifically associate these links with the shortcut module,
// since other modules may make them editable via the menu system.
// However, we do need to specify the correct menu name.
$link['menu_name'] = 'shortcut-' . $entity->id();
$link['plid'] = 0;
menu_link_save($link);
$menu_link->menu_name = 'shortcut-' . $entity->id();
$menu_link->plid = 0;
$menu_link->save();
}
}

View File

@ -66,7 +66,10 @@ class ShortcutLinksTest extends ShortcutTestBase {
theme_enable(array('seven'));
config('system.theme')->set('admin', 'seven')->save();
variable_set('node_admin_theme', TRUE);
$this->drupalGet($this->set->links[0]['link_path']);
$link = reset($this->set->links);
$this->drupalGet($link->link_path);
$this->assertRaw(t('Remove from %title shortcuts', array('%title' => $this->set->label())), '"Add to shortcuts" link properly switched to "Remove from shortcuts".');
}
@ -79,7 +82,8 @@ class ShortcutLinksTest extends ShortcutTestBase {
// Attempt to rename shortcut link.
$new_link_name = $this->randomName();
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'], array('shortcut_link[link_title]' => $new_link_name, 'shortcut_link[link_path]' => $set->links[0]['link_path']), t('Save'));
$link = reset($set->links);
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $link->mlid, array('shortcut_link[link_title]' => $new_link_name, 'shortcut_link[link_path]' => $link->link_path), t('Save'));
$saved_set = shortcut_set_load($set->id());
$titles = $this->getShortcutInformation($saved_set, 'link_title');
$this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name);
@ -95,7 +99,8 @@ class ShortcutLinksTest extends ShortcutTestBase {
// Tests changing a shortcut path.
$new_link_path = 'admin/config';
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'], array('shortcut_link[link_title]' => $set->links[0]['link_title'], 'shortcut_link[link_path]' => $new_link_path), t('Save'));
$link = reset($set->links);
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $link->mlid, array('shortcut_link[link_title]' => $link->link_title, 'shortcut_link[link_path]' => $new_link_path), t('Save'));
$saved_set = shortcut_set_load($set->id());
$paths = $this->getShortcutInformation($saved_set, 'link_path');
$this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path);
@ -108,10 +113,11 @@ class ShortcutLinksTest extends ShortcutTestBase {
function testShortcutLinkDelete() {
$set = $this->set;
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'] . '/delete', array(), 'Delete');
$link = reset($set->links);
$this->drupalPost('admin/config/user-interface/shortcut/link/' . $link->mlid . '/delete', array(), 'Delete');
$saved_set = shortcut_set_load($set->id());
$mlids = $this->getShortcutInformation($saved_set, 'mlid');
$this->assertFalse(in_array($set->links[0]['mlid'], $mlids), 'Successfully deleted a shortcut.');
$this->assertFalse(in_array($link->mlid, $mlids), 'Successfully deleted a shortcut.');
}
/**

View File

@ -91,7 +91,9 @@ class ShortcutSetsTest extends ShortcutTestBase {
$set = $this->set;
$old_mlids = $this->getShortcutInformation($set, 'mlid');
$set->links[] = $this->generateShortcutLink('admin', $this->randomName());
$menu_link = $this->generateShortcutLink('admin', $this->randomName());
$menu_link->save();
$set->links[$menu_link->uuid()] = $menu_link;
$set->save();
$saved_set = shortcut_set_load($set->id());

View File

@ -51,16 +51,20 @@ abstract class ShortcutTestBase extends WebTestBase {
// Populate the default shortcut set.
$shortcut_set = shortcut_set_load('default');
$shortcut_set->links[] = array(
$menu_link = entity_create('menu_link', array(
'link_path' => 'node/add',
'link_title' => st('Add content'),
'weight' => -20,
);
$shortcut_set->links[] = array(
));
$menu_link->save();
$shortcut_set->links[$menu_link->uuid()] = $menu_link;
$menu_item = entity_create('menu_link', array(
'link_path' => 'admin/content',
'link_title' => st('Find content'),
'weight' => -19,
);
));
$menu_item->save();
$shortcut_set->links[$menu_item->uuid()] = $menu_item;
$shortcut_set->save();
}
@ -84,11 +88,13 @@ abstract class ShortcutTestBase extends WebTestBase {
$set = entity_create('shortcut', array(
'id' => isset($id) ? $id : strtolower($this->randomName()),
'label' => empty($label) ? $this->randomString() : $label,
'links' => (!$default_links) ? array() : array(
$this->generateShortcutLink('node/add'),
$this->generateShortcutLink('admin/content'),
),
));
if ($default_links) {
$menu_link = $this->generateShortcutLink('node/add');
$set->links[$menu_link->uuid()] = $menu_link;
$menu_link = $this->generateShortcutLink('admin/content');
$set->links[$menu_link->uuid()] = $menu_link;
}
$set->save();
return $set;
}
@ -97,10 +103,11 @@ abstract class ShortcutTestBase extends WebTestBase {
* Creates a generic shortcut link.
*/
function generateShortcutLink($path, $title = '') {
$link = array(
$link = entity_create('menu_link', array(
'link_path' => $path,
'link_title' => !empty($title) ? $title : $this->randomName(),
);
));
$link->save();
return $link;
}
@ -121,8 +128,8 @@ abstract class ShortcutTestBase extends WebTestBase {
*/
function getShortcutInformation($set, $key) {
$info = array();
foreach ($set->links as $link) {
$info[] = $link[$key];
foreach ($set->links as $uuid => $link) {
$info[] = $link->{$key};
}
return $info;
}

View File

@ -138,7 +138,7 @@ function shortcut_set_switch_submit($form, &$form_state) {
$set = entity_create('shortcut', array(
'id' => $form_state['values']['id'],
'label' => $form_state['values']['label'],
'links' => menu_links_clone($default_set->links),
'links' => $default_set->links,
));
$set->save();
$replacements = array(
@ -209,7 +209,7 @@ function shortcut_set_customize($form, &$form_state, $shortcut_set) {
'links' => array(),
);
foreach ($shortcut_set->links as $link) {
foreach ($shortcut_set->links as $uuid => $link) {
$mlid = $link['mlid'];
$form['shortcuts']['links'][$mlid]['name']['#markup'] = l($link['link_title'], $link['link_path']);
$form['shortcuts']['links'][$mlid]['weight'] = array(
@ -362,10 +362,10 @@ function shortcut_link_edit($form, &$form_state, $shortcut_link) {
*/
function _shortcut_link_form_elements($shortcut_link = NULL) {
if (!isset($shortcut_link)) {
$shortcut_link = array(
$shortcut_link = entity_create('menu_link', array(
'link_title' => '',
'link_path' => ''
);
));
}
else {
$shortcut_link['link_path'] = ($shortcut_link['link_path'] == '<front>') ? '' : drupal_container()->get('path.alias_manager')->getPathAlias($shortcut_link['link_path']);
@ -421,7 +421,10 @@ function shortcut_link_edit_submit($form, &$form_state) {
}
$form_state['values']['shortcut_link']['link_path'] = $shortcut_path;
$shortcut_link = array_merge($form_state['values']['original_shortcut_link'], $form_state['values']['shortcut_link']);
$shortcut_link = $form_state['values']['original_shortcut_link'];
foreach ($form_state['values']['shortcut_link'] as $key => $value) {
$shortcut_link[$key] = $value;
}
menu_link_save($shortcut_link);
$set_name = str_replace('shortcut-', '' , $shortcut_link['menu_name']);
@ -460,9 +463,11 @@ function shortcut_admin_add_link($shortcut_link, &$shortcut_set) {
if (empty($shortcut_link['link_path'])) {
$shortcut_link['link_path'] = '<front>';
}
$menu_link = entity_create('menu_link', $shortcut_link);
$menu_link->save();
// Add the link to the end of the list.
$shortcut_set->links[] = $shortcut_link;
$shortcut_set->links[$menu_link->uuid()] = $menu_link;
shortcut_set_reset_link_weights($shortcut_set);
}

View File

@ -3,4 +3,5 @@ description = Allows users to manage customizable lists of shortcut links.
package = Core
version = VERSION
core = 8.x
dependencies[] = menu_link
configure = admin/config/user-interface/shortcut

View File

@ -337,17 +337,6 @@ function shortcut_set_load($id) {
return entity_load('shortcut', $id);
}
/**
* Implements hook_shortcut_load().
*
* Loads menu links attached to each of shortcuts.
*/
function shortcut_shortcut_load($entities) {
foreach ($entities as $id => $entity) {
$entity->set('links', menu_load_links('shortcut-' . $id));
}
}
/**
* Resets the link weights in a shortcut set to match their current order.
*
@ -361,9 +350,8 @@ function shortcut_shortcut_load($entities) {
*/
function shortcut_set_reset_link_weights(&$shortcut_set) {
$weight = -50;
foreach ($shortcut_set->links as &$link) {
$link['weight'] = $weight;
$weight++;
foreach ($shortcut_set->links as $menu_link) {
$menu_link->weight = ++$weight;
}
}
@ -566,7 +554,7 @@ function shortcut_preprocess_page(&$variables) {
$shortcut_set = shortcut_current_displayed_set();
// Check if $link is already a shortcut and set $link_mode accordingly.
foreach ($shortcut_set->links as $shortcut) {
foreach ($shortcut_set->links as $uuid => $shortcut) {
if ($link == $shortcut['link_path']) {
$mlid = $shortcut['mlid'];
break;

View File

@ -209,13 +209,13 @@ class BreadcrumbTest extends MenuTestBase {
$node2 = $this->drupalCreateNode(array(
'type' => $type,
'title' => $title,
'menu' => array(
'menu' => entity_create('menu_link', array(
'enabled' => 1,
'link_title' => 'Parent ' . $title,
'description' => '',
'menu_name' => $menu,
'plid' => 0,
),
)),
));
$nid2 = $node2->nid;
@ -237,13 +237,13 @@ class BreadcrumbTest extends MenuTestBase {
$node3 = $this->drupalCreateNode(array(
'type' => $type,
'title' => $title,
'menu' => array(
'menu' => entity_create('menu_link', array(
'enabled' => 1,
'link_title' => 'Child ' . $title,
'description' => '',
'menu_name' => $menu,
'plid' => $node2->menu['mlid'],
),
)),
));
$nid3 = $node3->nid;
@ -277,7 +277,8 @@ class BreadcrumbTest extends MenuTestBase {
'link_path' => 'node',
);
$this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$link = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => 'Root'))->fetchAssoc();
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Root'));
$link = reset($menu_links);
$edit = array(
'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'],
@ -335,10 +336,8 @@ class BreadcrumbTest extends MenuTestBase {
'parent' => "$menu:{$parent_mlid}",
);
$this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$tags[$name]['link'] = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
':title' => $edit['link_title'],
':href' => $edit['link_path'],
))->fetchAssoc();
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
$tags[$name]['link'] = reset($menu_links);
$tags[$name]['link']['link_path'] = $edit['link_path'];
$parent_mlid = $tags[$name]['link']['mlid'];
}
@ -436,20 +435,16 @@ class BreadcrumbTest extends MenuTestBase {
'link_path' => 'user',
);
$this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$link_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
':title' => $edit['link_title'],
':href' => $edit['link_path'],
))->fetchAssoc();
$menu_links_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
$link_user = reset($menu_links_user);
$edit = array(
'link_title' => $this->admin_user->name . ' link',
'link_path' => 'user/' . $this->admin_user->uid,
);
$this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$link_admin_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
':title' => $edit['link_title'],
':href' => $edit['link_path'],
))->fetchAssoc();
$menu_links_admin_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
$link_admin_user = reset($menu_links_admin_user);
// Verify expected breadcrumbs for the two separate links.
$this->drupalLogout();

View File

@ -26,7 +26,8 @@ class LinksTest extends WebTestBase {
*/
function createLinkHierarchy($module = 'menu_test') {
// First remove all the menu links.
db_truncate('menu_links')->execute();
$menu_links = menu_link_load_multiple();
menu_link_delete_multiple(array_keys($menu_links), TRUE, TRUE);
// Then create a simple link hierarchy:
// - $parent
@ -43,31 +44,36 @@ class LinksTest extends WebTestBase {
$links['parent'] = $base_options + array(
'link_path' => 'menu-test/parent',
);
menu_link_save($links['parent']);
$links['parent'] = entity_create('menu_link', $links['parent']);
$links['parent']->save();
$links['child-1'] = $base_options + array(
'link_path' => 'menu-test/parent/child-1',
'plid' => $links['parent']['mlid'],
);
menu_link_save($links['child-1']);
$links['child-1'] = entity_create('menu_link', $links['child-1']);
$links['child-1']->save();
$links['child-1-1'] = $base_options + array(
'link_path' => 'menu-test/parent/child-1/child-1-1',
'plid' => $links['child-1']['mlid'],
);
menu_link_save($links['child-1-1']);
$links['child-1-1'] = entity_create('menu_link', $links['child-1-1']);
$links['child-1-1']->save();
$links['child-1-2'] = $base_options + array(
'link_path' => 'menu-test/parent/child-1/child-1-2',
'plid' => $links['child-1']['mlid'],
);
menu_link_save($links['child-1-2']);
$links['child-1-2'] = entity_create('menu_link', $links['child-1-2']);
$links['child-1-2']->save();
$links['child-2'] = $base_options + array(
'link_path' => 'menu-test/parent/child-2',
'plid' => $links['parent']['mlid'],
);
menu_link_save($links['child-2']);
$links['child-2'] = entity_create('menu_link', $links['child-2']);
$links['child-2']->save();
return $links;
}

View File

@ -312,27 +312,30 @@ class MenuRouterTest extends WebTestBase {
$admin_user = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($admin_user);
$sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'";
$name = db_query($sql)->fetchField();
$this->assertEqual($name, 'original', 'Menu name is "original".');
$menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test'));
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->menu_name, 'original', 'Menu name is "original".');
// Change the menu_name parameter in menu_test.module, then force a menu
// rebuild.
menu_test_menu_name('changed');
menu_router_rebuild();
$sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'";
$name = db_query($sql)->fetchField();
$this->assertEqual($name, 'changed', 'Menu name was successfully changed after rebuild.');
$menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test'));
$menu_link = reset($menu_links);
$this->assertEqual($menu_link->menu_name, 'changed', 'Menu name was successfully changed after rebuild.');
}
/**
* Tests for menu hierarchy.
*/
function testMenuHierarchy() {
$parent_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent'))->fetchAssoc();
$child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc();
$unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc();
$parent_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent'));
$parent_link = reset($parent_links);
$child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child'));
$child_link = reset($child_links);
$unattached_child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child2/child'));
$unattached_child_link = reset($unattached_child_links);
$this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.');
$this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.');
@ -343,12 +346,16 @@ class MenuRouterTest extends WebTestBase {
*/
function testMenuHidden() {
// Verify links for one dynamic argument.
$links = db_select('menu_links', 'ml')
->fields('ml')
->condition('ml.router_path', 'menu-test/hidden/menu%', 'LIKE')
->orderBy('ml.router_path')
->execute()
->fetchAllAssoc('router_path', PDO::FETCH_ASSOC);
$query = entity_query('menu_link')
->condition('router_path', 'menu-test/hidden/menu', 'STARTS_WITH')
->sort('router_path');
$result = $query->execute();
$menu_links = menu_link_load_multiple($result);
$links = array();
foreach ($menu_links as $menu_link) {
$links[$menu_link->router_path] = $menu_link;
}
$parent = $links['menu-test/hidden/menu'];
$depth = $parent['depth'] + 1;
@ -391,12 +398,16 @@ class MenuRouterTest extends WebTestBase {
$this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid)));
// Verify links for two dynamic arguments.
$links = db_select('menu_links', 'ml')
->fields('ml')
->condition('ml.router_path', 'menu-test/hidden/block%', 'LIKE')
->orderBy('ml.router_path')
->execute()
->fetchAllAssoc('router_path', PDO::FETCH_ASSOC);
$query = entity_query('menu_link')
->condition('router_path', 'menu-test/hidden/block', 'STARTS_WITH')
->sort('router_path');
$result = $query->execute();
$menu_links = menu_link_load_multiple($result);
$links = array();
foreach ($menu_links as $menu_link) {
$links[$menu_link->router_path] = $menu_link;
}
$parent = $links['menu-test/hidden/block'];
$depth = $parent['depth'] + 1;
@ -472,7 +483,7 @@ class MenuRouterTest extends WebTestBase {
*/
function testMenuLinkOptions() {
// Create a menu link with options.
$menu_link = array(
$menu_link = entity_create('menu_link', array(
'link_title' => 'Menu link options test',
'link_path' => 'test-page',
'module' => 'menu_test',
@ -484,7 +495,7 @@ class MenuRouterTest extends WebTestBase {
'testparam' => 'testvalue',
),
),
);
));
menu_link_save($menu_link);
// Load front page.

View File

@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Menu;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
use Drupal\simpletest\UnitTestBase;
/**
@ -16,13 +17,7 @@ class TreeDataUnitTest extends UnitTestBase {
/**
* Dummy link structure acceptable for menu_tree_data().
*/
var $links = array(
1 => array('mlid' => 1, 'depth' => 1),
2 => array('mlid' => 2, 'depth' => 1),
3 => array('mlid' => 3, 'depth' => 2),
4 => array('mlid' => 4, 'depth' => 3),
5 => array('mlid' => 5, 'depth' => 1),
);
protected $links = array();
public static function getInfo() {
return array(
@ -35,7 +30,15 @@ class TreeDataUnitTest extends UnitTestBase {
/**
* Validate the generation of a proper menu tree hierarchy.
*/
function testMenuTreeData() {
public function testMenuTreeData() {
$this->links = array(
1 => new MenuLink(array('mlid' => 1, 'depth' => 1), 'menu_link'),
2 => new MenuLink(array('mlid' => 2, 'depth' => 1), 'menu_link'),
3 => new MenuLink(array('mlid' => 3, 'depth' => 2), 'menu_link'),
4 => new MenuLink(array('mlid' => 4, 'depth' => 3), 'menu_link'),
5 => new MenuLink(array('mlid' => 5, 'depth' => 1), 'menu_link'),
);
$tree = menu_tree_data($this->links);
// Validate that parent items #1, #2, and #5 exist on the root level.

View File

@ -7,33 +7,19 @@
namespace Drupal\system\Tests\Menu;
use Drupal\simpletest\WebTestBase;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Menu tree output related tests.
*/
class TreeOutputTest extends WebTestBase {
class TreeOutputTest extends DrupalUnitTestBase {
public static $modules = array('system', 'menu_link');
/**
* Dummy link structure acceptable for menu_tree_output().
*/
var $tree_data = array(
'1'=> array(
'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ),
'below' => array(
'2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ),
'below' => array(
'3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ),
'below' => array() ),
'4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ),
'below' => array() )
)
)
)
),
'5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
'6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
'7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) )
);
protected $tree_data = array();
public static function getInfo() {
return array(
@ -45,12 +31,35 @@ class TreeOutputTest extends WebTestBase {
function setUp() {
parent::setUp();
$this->installSchema('system', 'menu_router');
}
/**
* Validate the generation of a proper menu tree output.
*/
function testMenuTreeData() {
$storage_controller = $this->container->get('plugin.manager.entity')->getStorageController('menu_link');
// @todo Prettify this tree buildup code, it's very hard to read.
$this->tree_data = array(
'1'=> array(
'link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 1, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')))),
'below' => array(
'2' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 2, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')))),
'below' => array(
'3' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 3, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')))),
'below' => array() ),
'4' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 4, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')))),
'below' => array() )
)
)
)
),
'5' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 5, 'hidden' => 1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access' => 1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()),
'6' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 6, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()),
'7' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 7, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access' => 1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array())
);
$output = menu_tree_output($this->tree_data);
// Validate that the - in main-menu is changed into an underscore

View File

@ -19,33 +19,39 @@ function system_admin_config_page() {
drupal_set_message(t('One or more problems were detected with your Drupal installation. Check the <a href="@status">status report</a> for more information.', array('@status' => url('admin/reports/status'))), 'error');
}
$blocks = array();
if ($admin = db_query("SELECT menu_name, mlid FROM {menu_links} WHERE link_path = 'admin/config' AND module = 'system'")->fetchAssoc()) {
$result = db_query("
SELECT m.*, ml.*
FROM {menu_links} ml
INNER JOIN {menu_router} m ON ml.router_path = m.path
WHERE ml.link_path <> 'admin/help' AND menu_name = :menu_name AND ml.plid = :mlid AND hidden = 0", $admin, array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $item) {
_menu_link_translate($item);
if (!$item['access']) {
continue;
}
// The link description, either derived from 'description' in hook_menu()
// or customized via menu module is used as title attribute.
if (!empty($item['localized_options']['attributes']['title'])) {
$item['description'] = $item['localized_options']['attributes']['title'];
unset($item['localized_options']['attributes']['title']);
}
$block = $item;
$block['content'] = '';
$block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($item)));
if (!empty($block['content'])) {
$block['show'] = TRUE;
}
if ($system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/config', 'module' => 'system'))) {
$system_link = reset($system_link);
$query = entity_query('menu_link')
->condition('link_path', 'admin/help', '<>')
->condition('menu_name', $system_link->menu_name)
->condition('plid', $system_link->id())
->condition('hidden', 0);
$result = $query->execute();
if (!empty($result)) {
$menu_links = menu_link_load_multiple($result);
// Prepare for sorting as in function _menu_tree_check_access().
// The weight is offset so it is always positive, with a uniform 5-digits.
$blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block;
foreach ($menu_links as $item) {
_menu_link_translate($item);
if (!$item['access']) {
continue;
}
// The link description, either derived from 'description' in hook_menu()
// or customized via menu module is used as title attribute.
if (!empty($item['localized_options']['attributes']['title'])) {
$item['description'] = $item['localized_options']['attributes']['title'];
unset($item['localized_options']['attributes']['title']);
}
$block = $item;
$block['content'] = '';
$block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($item)));
if (!empty($block['content'])) {
$block['show'] = TRUE;
}
// Prepare for sorting as in function _menu_tree_check_access().
// The weight is offset so it is always positive, with a uniform 5-digits.
$blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block;
}
}
}
if ($blocks) {

View File

@ -897,127 +897,6 @@ function hook_menu_alter(&$items) {
$items['node/add']['access callback'] = FALSE;
}
/**
* Alter the data being saved to the {menu_links} table by menu_link_save().
*
* @param $item
* Associative array defining a menu link as passed into menu_link_save().
*
* @see hook_translated_menu_link_alter()
*/
function hook_menu_link_alter(&$item) {
// Make all new admin links hidden (a.k.a disabled).
if (strpos($item['link_path'], 'admin') === 0 && empty($item['mlid'])) {
$item['hidden'] = 1;
}
// Flag a link to be altered by hook_translated_menu_link_alter().
if ($item['link_path'] == 'devel/cache/clear') {
$item['options']['alter'] = TRUE;
}
// Flag a link to be altered by hook_translated_menu_link_alter(), but only
// if it is derived from a menu router item; i.e., do not alter a custom
// menu link pointing to the same path that has been created by a user.
if ($item['link_path'] == 'user' && $item['module'] == 'system') {
$item['options']['alter'] = TRUE;
}
}
/**
* Alter a menu link after it has been translated and before it is rendered.
*
* This hook is invoked from _menu_link_translate() after a menu link has been
* translated; i.e., after dynamic path argument placeholders (%) have been
* replaced with actual values, the user access to the link's target page has
* been checked, and the link has been localized. It is only invoked if
* $item['options']['alter'] has been set to a non-empty value (e.g., TRUE).
* This flag should be set using hook_menu_link_alter().
*
* Implementations of this hook are able to alter any property of the menu link.
* For example, this hook may be used to add a page-specific query string to all
* menu links, or hide a certain link by setting:
* @code
* 'hidden' => 1,
* @endcode
*
* @param $item
* Associative array defining a menu link after _menu_link_translate()
* @param $map
* Associative array containing the menu $map (path parts and/or objects).
*
* @see hook_menu_link_alter()
*/
function hook_translated_menu_link_alter(&$item, $map) {
if ($item['href'] == 'devel/cache/clear') {
$item['localized_options']['query'] = drupal_get_destination();
}
}
/**
* Inform modules that a menu link has been created.
*
* This hook is used to notify modules that menu items have been
* created. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param $link
* Associative array defining a menu link as passed into menu_link_save().
*
* @see hook_menu_link_update()
* @see hook_menu_link_delete()
*/
function hook_menu_link_insert($link) {
// In our sample case, we track menu items as editing sections
// of the site. These are stored in our table as 'disabled' items.
$record['mlid'] = $link['mlid'];
$record['menu_name'] = $link['menu_name'];
$record['status'] = 0;
drupal_write_record('menu_example', $record);
}
/**
* Inform modules that a menu link has been updated.
*
* This hook is used to notify modules that menu items have been
* updated. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param $link
* Associative array defining a menu link as passed into menu_link_save().
*
* @see hook_menu_link_insert()
* @see hook_menu_link_delete()
*/
function hook_menu_link_update($link) {
// If the parent menu has changed, update our record.
$menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $link['mlid']))->fetchField();
if ($menu_name != $link['menu_name']) {
db_update('menu_example')
->fields(array('menu_name' => $link['menu_name']))
->condition('mlid', $link['mlid'])
->execute();
}
}
/**
* Inform modules that a menu link has been deleted.
*
* This hook is used to notify modules that menu items have been
* deleted. Contributed modules may use the information to perform
* actions based on the information entered into the menu system.
*
* @param $link
* Associative array defining a menu link as passed into menu_link_save().
*
* @see hook_menu_link_insert()
* @see hook_menu_link_update()
*/
function hook_menu_link_delete($link) {
// Delete the record from our table.
db_delete('menu_example')
->condition('mlid', $link['mlid'])
->execute();
}
/**
* Alter tabs and actions displayed on the page before they are rendered.
*

View File

@ -964,192 +964,6 @@ function system_schema() {
'primary key' => array('path'),
);
$schema['menu_links'] = array(
'description' => 'Contains the individual links within a menu.',
'fields' => array(
'menu_name' => array(
'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'mlid' => array(
'description' => 'The menu link ID (mlid) is the integer primary key.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'plid' => array(
'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'link_path' => array(
'description' => 'The Drupal path or external path this link points to.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'router_path' => array(
'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'link_title' => array(
'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'translatable' => TRUE,
),
'options' => array(
'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
'type' => 'blob',
'not null' => FALSE,
'translatable' => TRUE,
),
'module' => array(
'description' => 'The name of the module that generated this link.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'system',
),
'hidden' => array(
'description' => 'A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'external' => array(
'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'has_children' => array(
'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'expanded' => array(
'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'weight' => array(
'description' => 'Link weight among links in the same menu at the same depth.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'depth' => array(
'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'customized' => array(
'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'p1' => array(
'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the plid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p2' => array(
'description' => 'The second mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p3' => array(
'description' => 'The third mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p4' => array(
'description' => 'The fourth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p5' => array(
'description' => 'The fifth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p6' => array(
'description' => 'The sixth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p7' => array(
'description' => 'The seventh mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p8' => array(
'description' => 'The eighth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p9' => array(
'description' => 'The ninth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'updated' => array(
'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
),
'indexes' => array(
'path_menu' => array(array('link_path', 128), 'menu_name'),
'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'),
'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
'router_path' => array(array('router_path', 128)),
),
'primary key' => array('mlid'),
);
$schema['queue'] = array(
'description' => 'Stores items in queues.',
'fields' => array(
@ -2244,6 +2058,42 @@ function system_update_8047() {
));
}
/**
* Enable the new Menu link module.
*
* Creates the langcode and UUID columns for menu links.
*/
function system_update_8048() {
// Enable the module without re-installing the schema.
update_module_enable(array('menu_link'));
// Add the langcode column if it doesn't exist.
if (!db_field_exists('menu_links', 'langcode')) {
$column = array(
'description' => 'The {language}.langcode of this entity.',
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
);
db_add_field('menu_links', 'langcode', $column);
}
// Add the UUID column.
$column = array(
'description' => 'Unique Key: Universally unique identifier for this entity.',
'type' => 'varchar',
'length' => 128,
'not null' => FALSE,
);
$keys = array(
'unique keys' => array(
'uuid' => array('uuid'),
),
);
db_add_field('menu_links', 'uuid', $column, $keys);
}
/**
* @} End of "defgroup updates-7.x-to-8.x".
* The next series of updates should start at 9000.

View File

@ -2677,7 +2677,10 @@ function system_admin_menu_block($item) {
}
if (!isset($item['mlid'])) {
$item += db_query("SELECT mlid, menu_name FROM {menu_links} ml WHERE ml.router_path = :path AND module = 'system'", array(':path' => $item['path']))->fetchAssoc();
$menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => $item['path'], 'module' => 'system'));
$menu_link = reset($menu_links);
$item['mlid'] = $menu_link->id();
$item['menu_name'] = $menu_link->menu_name;
}
if (isset($cache[$item['mlid']])) {
@ -2685,17 +2688,8 @@ function system_admin_menu_block($item) {
}
$content = array();
$query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
$query->join('menu_router', 'm', 'm.path = ml.router_path');
$query
->fields('ml')
// Weight should be taken from {menu_links}, not {menu_router}.
->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight')))
->condition('ml.plid', $item['mlid'])
->condition('ml.menu_name', $item['menu_name'])
->condition('ml.hidden', 0);
foreach ($query->execute() as $link) {
$menu_links = entity_load_multiple_by_properties('menu_link', array('plid' => $item['mlid'], 'menu_name' => $item['menu_name'], 'hidden' => 0));
foreach ($menu_links as $link) {
_menu_link_translate($link);
if ($link['access']) {
// The link description, either derived from 'description' in
@ -3460,18 +3454,8 @@ function system_get_module_admin_tasks($module, $info) {
if (!isset($links)) {
$links = array();
$query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
$query->join('menu_router', 'm', 'm.path = ml.router_path');
$query
->fields('ml')
// Weight should be taken from {menu_links}, not {menu_router}.
->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight')))
->condition('ml.link_path', 'admin/%', 'LIKE')
->condition('ml.hidden', 0, '>=')
->condition('ml.module', 'system')
->condition('m.number_parts', 1, '>')
->condition('m.page_callback', 'system_admin_menu_block_page', '<>');
foreach ($query->execute() as $link) {
$menu_links = entity_get_controller('menu_link')->loadModuleAdminTasks();
foreach ($menu_links as $link) {
_menu_link_translate($link);
if ($link['access']) {
$links[$link['router_path']] = $link;
@ -3523,6 +3507,7 @@ function system_get_module_admin_tasks($module, $info) {
$item['title'] = t('Configure @module permissions', array('@module' => $info['name']));
unset($item['description']);
$item['localized_options']['fragment'] = 'module-' . $module;
$item = entity_create('menu_link', $item);
$admin_tasks["admin/people/permissions#module-$module"] = $item;
}
}

View File

@ -5,5 +5,6 @@ package = Core
version = VERSION
dependencies[] = breakpoint
dependencies[] = menu_link
configure = admin/structure/toolbar

View File

@ -511,8 +511,13 @@ function toolbar_toolbar() {
*/
function toolbar_get_menu_tree() {
$tree = array();
$admin_link = db_query('SELECT * FROM {menu_links} WHERE menu_name = :menu_name AND module = :module AND link_path = :path', array(':menu_name' => 'admin', ':module' => 'system', ':path' => 'admin'))->fetchAssoc();
if ($admin_link) {
$query = entity_query('menu_link')
->condition('menu_name', 'admin')
->condition('module', 'system')
->condition('link_path', 'admin');
$result = $query->execute();
if (!empty($result)) {
$admin_link = menu_link_load(reset($result));
$tree = menu_build_tree('admin', array(
'expanded' => array($admin_link['mlid']),
'min_depth' => $admin_link['depth'] + 1,

View File

@ -1119,21 +1119,21 @@ function user_menu_site_status_alter(&$menu_site_status, $path) {
}
/**
* Implements hook_menu_link_alter().
* Implements hook_menu_link_presave().
*/
function user_menu_link_alter(&$link) {
function user_menu_link_presave($link) {
// The path 'user' must be accessible for anonymous users, but only visible
// for authenticated users. Authenticated users should see "My account", but
// anonymous users should not see it at all. Therefore, invoke
// user_translated_menu_link_alter() to conditionally hide the link.
if ($link['link_path'] == 'user' && $link['module'] == 'system') {
$link['options']['alter'] = TRUE;
// user_menu_link_load() to conditionally hide the link.
if ($link->link_path == 'user' && $link->module == 'system') {
$link->options['alter'] = TRUE;
}
// Force the Logout link to appear on the top-level of 'account' menu by
// default (i.e., unless it has been customized).
if ($link['link_path'] == 'user/logout' && $link['module'] == 'system' && empty($link['customized'])) {
$link['plid'] = 0;
if ($link->link_path == 'user/logout' && $link->module == 'system' && empty($link->customized)) {
$link->plid = 0;
}
}
@ -1149,12 +1149,14 @@ function user_menu_breadcrumb_alter(&$active_trail, $item) {
}
/**
* Implements hook_translated_menu_link_alter().
* Implements hook_menu_link_load().
*/
function user_translated_menu_link_alter(&$link) {
function user_menu_link_load($menu_links) {
// Hide the "User account" link for anonymous users.
if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) {
$link['hidden'] = 1;
foreach ($menu_links as $link) {
if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) {
$link['hidden'] = 1;
}
}
}

View File

@ -37,6 +37,7 @@ class FilterEqualityTest extends ViewUnitTestBase {
parent::setUp();
$this->enableModules(array('system'));
$this->enableModules(array('menu_link'));
}
function viewsData() {

View File

@ -38,6 +38,7 @@ class FilterInOperatorTest extends ViewUnitTestBase {
parent::setUp();
$this->enableModules(array('system'));
$this->enableModules(array('menu_link'));
}
function viewsData() {

View File

@ -38,6 +38,7 @@ class FilterNumericTest extends ViewUnitTestBase {
parent::setUp();
$this->enableModules(array('system'));
$this->enableModules(array('menu_link'));
}
function viewsData() {

View File

@ -37,6 +37,7 @@ class FilterStringTest extends ViewUnitTestBase {
parent::setUp();
$this->enableModules(array('system'));
$this->enableModules(array('menu_link'));
}
function viewsData() {

View File

@ -236,12 +236,12 @@ function standard_install() {
->execute();
// Create a Home link in the main menu.
$item = array(
$menu_link = entity_create('menu_link', array(
'link_title' => st('Home'),
'link_path' => '<front>',
'menu_name' => 'main',
);
menu_link_save($item);
));
$menu_link->save();
// Enable the Contact link in the footer menu.
menu_link_maintain('system', 'enable', 'contact');
@ -250,16 +250,22 @@ function standard_install() {
// Populate the default shortcut set.
$shortcut_set = shortcut_set_load('default');
$shortcut_set->links[] = array(
$menu_link = entity_create('menu_link', array(
'link_path' => 'node/add',
'link_title' => st('Add content'),
'weight' => -20,
);
$shortcut_set->links[] = array(
));
$menu_link->save();
$shortcut_set->links[$menu_link->uuid()] = $menu_link;
$menu_link = entity_create('menu_link', array(
'link_path' => 'admin/content',
'link_title' => st('Find content'),
'weight' => -19,
);
));
$menu_link->save();
$shortcut_set->links[$menu_link->uuid()] = $menu_link;
$shortcut_set->save();
// Enable the admin theme.