4

Or: how can I prevent unpublished nodes' menu items to be removed in the menu select when the user does not have the bypass node access checks permission, but may actually edit various unpublished nodes?

Im facing the problem that in the node form, the menu item options are missing some menu items because the related node is unpublished although the user may actually see and edit those nodes.

In /core/lib/Drupal/Core/Menu/MenuParentFormSelector.php the function getParentSelectOptions builds the options list which is then used in the node edit forms.

The function is:

  /**
   * {@inheritdoc}
   */
  public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL) {
    if (!isset($menus)) {
      $menus = $this->getMenuOptions();
    }

    $options = array();
    $depth_limit = $this->getParentDepthLimit($id);
    foreach ($menus as $menu_name => $menu_title) {
      $options[$menu_name . ':'] = '<' . $menu_title . '>';

      $parameters = new MenuTreeParameters();
      $parameters->setMaxDepth($depth_limit);
      $tree = $this->menuLinkTree->load($menu_name, $parameters);
      $manipulators = array(
        array('callable' => 'menu.default_tree_manipulators:checkNodeAccess'),
        array('callable' => 'menu.default_tree_manipulators:checkAccess'),
        array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
      );
      $tree = $this->menuLinkTree->transform($tree, $manipulators);
      $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit, $cacheability);
    }
    return $options;
  }

The critical part in that function is the manipulator 'menu.default_tree_manipulators:checkNodeAccess'.

The manipulator (/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php) does the following thing (public function checkNodeAccess):

if ($this->account->hasPermission('bypass node access')) {
  $query->accessCheck(FALSE);
}
else {
  $access_result->addCacheContexts(['user.node_grants:view']);
  $query->condition('status', NODE_PUBLISHED); // <<< wtf?
}

TL;DR

So it assumes "either a user may bypass all node access checks or hide all unpublished nodes from the menu select" - which is weird...

How can I overwrite this?

2 Answers 2

5

I was unable to find a way to alter tree manipulators of an existing menu. But it is possible if you render menu, let's say in a custom block.

Create your own tree manipulator service.

Copied from core.services.yml:

  menu.default_tree_manipulators:
    class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
    arguments: ['@access_manager', '@current_user', '@entity_type.manager']

you can define your service in a similar way:

  mymodule.my_custom_tree_manipulators:
    class: Drupal\mymodule\MyCustomMenuLinkTreeManipulators
    arguments: ['@access_manager', '@current_user', '@entity_type.manager']

Then create MyCustomMenuLinkTreeManipulators class that extends DefaultMenuLinkTreeManipulators, define custom method, let's say checkAccessIgnoreStatus() and implement logic you need.

There are many ways to use it, I'd create a new block plugin for that. Refer to my answer for details on how to render menu with manipulators.

Use your manipulators:

$manipulators = array(
        array('callable' => 'mymodule.my_custom_tree_manipulators:checkAccessIgnoreStatus'),
        // ...
      );
3

I have got similar task. I have to override parentSelectOptionsTreeWalk method from this class. It is solution: add this to services.yml in sites/default

services:
  menu.parent_form_selector:
      class: Drupal\my_page\MyPageMenuParentFormSelector
      arguments: ['@menu.link_tree', '@entity.manager', '@string_translation']

next create in my_page module in src directory file MyPageServiceProvider.php

<?php

namespace Drupal\my_page;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class MyPageServiceProvider.
*
* @package Drupal\my_page
*/

class MyPageServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/

public function alter(ContainerBuilder $container) {
$definition = $container->getDefinition('menu.parent_form_selector');
$definition->setClass('Drupal\my_page\MyPageMenuParentFormSelector');
$definition->setArguments(
  [
    new Reference('menu.link_tree'),
    new Reference('entity.manager'),
    new Reference('string_translation'),
  ]
);
}
}

and create your class: MyPagemenuParentFormSelector.php

<?php

    namespace Drupal\my_page;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Menu\MenuParentFormSelector;

/**
 * Default implementation of the menu parent form selector service.
 *
 * The form selector is a list of all appropriate menu links.
 */

class MyPageMenuParentFormSelector extends MenuParentFormSelector implements MenuParentFormSelectorInterface {
  use StringTranslationTrait;

  /**
   * The menu link tree service.
   *
   * @var \Drupal\Core\Menu\MenuLinkTreeInterface
   */
  protected $menuLinkTree;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityManagerInterface
   */
  protected $entityManager;

  /**
   * Iterates over all items in the tree to prepare the parents select options.
   *
   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
   *   The menu tree.
   * @param string $menu_name
   *   The menu name.
   * @param string $indent
   *   The indentation string used for the label.
   * @param array $options
   *   The select options.
   * @param string $exclude
   *   An excluded menu link.
   * @param int $depth_limit
   *   The maximum depth of menu links considered for the select options.
   * @param \Drupal\Core\Cache\CacheableMetadata|null &$cacheability
   *   The object to add cacheability metadata to, if not NULL.
   */
  protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit, CacheableMetadata &$cacheability = NULL) {
    foreach ($tree as $element) {
      if ($element->depth > $depth_limit) {
// Don't iterate through any links on this level.
        break;
      }

// Collect the cacheability metadata of the access result, as well as the
// link.
      if ($cacheability) {
        $cacheability = $cacheability
          ->merge(CacheableMetadata::createFromObject($element->access))
          ->merge(CacheableMetadata::createFromObject($element->link));
      }

// Only show accessible links.
      if (!$element->access->isAllowed()) {
        continue;
      }

      $link = $element->link;
      if ($link->getPluginId() != $exclude) {
        $title = $indent . ' ' . Unicode::truncate($link->getTitle(), 255, TRUE, FALSE);
        if (!$link->isEnabled()) {
          $title .= ' (' . $this->t('disabled') . ')';
        }
        $options[$menu_name . ':' . $link->getPluginId()] = $title;
        if (!empty($element->subtree)) {
          $this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit, $cacheability);
        }
      }
    }
  }
}

Here is good info about override drupal core services https://www.hs2solutions.com/blog/drupal-how-override-core-drupal-8-service

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.