Currently, hook_menu_link_defaults() has the following documentation:

/**
 * Alter links for menus.
 *
 * @param array $links
 *   The link definitions to be altered.
 *
 * @return array
 *   An array of default menu links. Each link has a key that is the machine
 *   name, which must be unique. By default, use the route name as the
 *   machine name. In cases where multiple links use the same route name, such
 *   as two links to the same page in different menus, or two links using the
 *   same route name but different route parameters, the suggested machine name
 *   patten is the route name followed by a dot and a unique suffix. For
 *   example, an additional logout link might have a machine name of
 *   user.logout.navigation, and default links provided to edit the article and
 *   page content types could use machine names node.type_edit.article and
 *   node.type_edit.page. Since the machine name may be arbitrary, you should
 *   never write code that assumes it is identical to the route name.
 *
 *   The value corresponding to each machine name key is an associative array
 *   that may contain the following key-value pairs:
 *   - title: (required) The untranslated title of the menu link.
 *   - description: The untranslated description of the link.
 *   - route_name: (optional) The route name to be used to build the path.
 *     Either a route_name or a link_path must be provided.
 *   - route_parameters: (optional) The route parameters to build the path.
 *   - link_path: (optional) If you have an external link use link_path instead
 *     of providing a route_name.
 *   - parent: (optional) The machine name of the link that is this link's menu
 *     parent.
 *   - weight: (optional) An integer that determines the relative position of
 *     items in the menu; higher-weighted items sink. Defaults to 0. Menu items
 *     with the same weight are ordered alphabetically.
 *   - menu_name: (optional) The machine name of a menu to put the link in, if
 *     not the default Tools menu.
 *   - expanded: (optional) If set to TRUE, and if a menu link is provided for
 *     this menu item (as a result of other properties), then the menu link is
 *     always expanded, equivalent to its 'always expanded' checkbox being set
 *     in the UI.
 *   - options: (optional) An array of options to be passed to l() when
 *     generating a link from this menu item.
 */
function hook_menu_link_defaults_alter(&$links) {
  // Change the weight and title of the user.logout link.
  $links['user.logout']['weight'] = -10;
  $links['user.logout']['title'] = 'Logout';
}

And dblog module adds a new item at the following code:

/**
 * Implements hook_menu_link_defaults_alter().
 */
function dblog_menu_link_defaults_alter(&$links) {
  if (\Drupal::moduleHandler()->moduleExists('search')) {
    $links['dblog.search'] = array(
      'title' => 'Top search phrases',
      'route_name' => 'dblog.search',
      'description' => 'View most popular search phrases.',
      'parent' => 'system.admin_reports',
    );
  }

  return $links;
}

Problem is, since the above array does not define $links['dblog.search']['machine_name'], the menu link is saved like the following:

*************************** 2. row ***************************
       menu_name: admin
            mlid: 62
            uuid: 03c5fb18-290e-4c50-9c04-4a14903bc478
    machine_name: NULL
            plid: 45
       link_path: admin/reports/search
        langcode: 
      link_title: Top search phrases
         options: a:1:{s:10:"attributes";a:1:{s:5:"title";s:33:"View most popular search phrases.";}}
          module: menu_ui
          hidden: 0
        external: 0
    has_children: 0
        expanded: 0
          weight: 0
           depth: 3
      customized: 0
              p1: 4
              p2: 45
              p3: 62
              p4: 0
              p5: 0
              p6: 0
              p7: 0
              p8: 0
              p9: 0
         updated: 0
      route_name: dblog.search
route_parameters: a:0:{}

Since, machine_name = NULL, the following code at menu.inc does not find the link definition while rebuilding:

function menu_link_rebuild_defaults() {
...
  if ($all_links) {
    foreach ($all_links as $machine_name => $link) {
      // 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('machine_name', $machine_name)
        ->execute()->fetchObject();
      if ($existing_item) {
...

This causes that the menu link gets listed several times (as many as you clear caches), as you can see in the following screenshot:

Repeated menu links

Possible solutions

a) Specify at hook_menu_link_defaults() that new links must define machine_name.
b) Add logic at _menu_link_save_recursive() to make sure that 'machine_name' is set before calling drupal_save_record().

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

juampynr’s picture

Status: Active » Needs review
FileSize
481 bytes

Here is a patch that fixes the issue by adding machine_name to dblog_menu_links_defaults_alter().

juampynr’s picture

Here is a perhaps cleaner way to solve it in which we set the machine_name index in case it is not present after loading all the links.

dawehner’s picture

This seems fine, but we are gonna remove this file anyway.

juampynr’s picture

@dawehner, what's the plan? Is there any post where I can read about it and help?

dawehner’s picture

dawehner’s picture

juampynr’s picture

No point then in committing this until those issues are fixed? This issue is affecting Devel module too #2244523: Fix devel after all the menu system changes.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Well yeah ... rebase that branch won't be funny anyway :P

dawehner’s picture

Not sure whether we though need some test coverage

webchick’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Agreed, I think test coverage would be a good idea. We don't want to ever accidentally re-introduce this bug.

juampynr’s picture

Here it is. Added a patch with just the test to make it fail and a patch with the test and the fix.

The last submitted patch, 11: drupal-menu-links-machine-name-2258299-11-just-test.patch, failed testing.

juampynr’s picture

Priority: Normal » Critical

Bumping up as critical since it meets "Render a system unusable and have no workaround.".

If caches are rebuild 100 times (very common on a production site), the admin/reports page would show 100 links to Top Search Phrases. Devel module is also waiting for this to get committed before fixing #2244523: Fix devel after all the menu system changes.

dawehner’s picture

Tbh. issue priorities are more or less random these days.

+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -179,6 +180,15 @@ protected function doTestMenuName() {
   /**
+   * Tests menu links added in hook_menu_link_defaults_alter().
+   */
+  protected function doTestMenuLinkDefaultsAlter() {
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('route_name' => 'menu_test.custom'));
+    $menu_link = reset($menu_links);
+    $this->assertEqual($menu_link->machine_name, 'menu_test.custom', 'Menu links added at hook_menu_link_defaults_alter() obtain the machine name from the $links key.');
+  }

It would be great to also have something which tries to test the actual regression: you execute the rebuild multiple times and end up with more than one entry. This should not be that hard to test.

pwolanin’s picture

juampynr’s picture

Added an assertion that rebuilds the menu tree and checks that links added at hook_menu_link_defaults() do not duplicate.

Status: Needs review » Needs work

The last submitted patch, 16: drupal-menu-links-machine-name-2258299-16-just-test.patch, failed testing.

juampynr’s picture

Status: Needs work » Needs review

Setting to needs review. Tests worked as expected.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

It is great to see some test coverage, this would be yet another critical which would be also covered by the menu rewrite. So sad.

  • Commit 50197bc on 8.x by alexpott:
    Issue #2258299 by juampy: Dblog_menu_link_defaults_alter() must set '...
alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs tests

Committed 50197bc and pushed to 8.x. Thanks!

Nice to have test coverage.

diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index 4c2f7cc..f8aff5d 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -11,18 +11,13 @@
  * Implements hook_menu_link_defaults_alter().
  */
 function menu_test_menu_link_defaults_alter(&$links) {
-  /**
-   *
-   * Many of the machine names here are slightly different from the route name.
-   * Since the machine name is arbitrary, this helps ensure that core does not
-   * add mistaken assumptions about the correlation.
-   */
+  // Many of the machine names here are slightly different from the route name.
+  // Since the machine name is arbitrary, this helps ensure that core does not
+  // add mistaken assumptions about the correlation.
   $links['menu_test.menu_name_test']['menu_name'] = menu_test_menu_name();
   $links['menu_test.context']['title'] = \Drupal::config('menu_test.menu_item')->get('title');
 
-  /**
-   * Adds a custom menu link.
-   */
+  // Adds a custom menu link.
   $links['menu_test.custom'] = array(
     'title' => 'Custom link',
     'route_name' => 'menu_test.custom',

Fixed comment style during commit.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.