Problem/Motivation

It's possible to get a EntityStorageException when adding a new menu link that is above the maximum depth.

Steps to reproduce:

  1. Install Drupal 8 with the Standard profile.
  2. Keep adding nested child links to a menu, the Administration menu is good for testing because it already has 4 levels (/admin/structure/menu/manage/admin).

See an exception similar to the following:

Drupal\Core\Entity\EntityStorageException: The link with ID menu_link_content:09136983-dc5b-42a2-b000-f9365ce4e25c or its children exceeded the maximum depth of 9 in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 880 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

Proposed resolution

Add validation to the add menu link form so that this doesn't happen and/or limit the Parent link <select> to only display items that meet the criteria, like on the edit menu link form.

Remaining tasks

  • Patch needs to be written
  • Automated tests added to test this functionality (likely a new test method on \Drupal\menu_ui\Tests\MenuTest)

User interface changes

n/a

API changes

n/a

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

star-szr’s picture

Issue summary: View changes
star-szr’s picture

Issue summary: View changes
star-szr’s picture

Issue summary: View changes
Upchuk’s picture

Assigned: Unassigned » Upchuk

I can take a look at this.

Upchuk’s picture

Status: Active » Needs review
Issue tags: +Amsterdam2014, +Needs tests
FileSize
4.39 KB

Here is the patch fixing this issue.

I injected the MenuLinkManager and Menu\MenuLinkTree into the form to use them to get the depth of the parent menu link that is selected.

Please tell me if there is a better way of achieving this. For now, this works it seems

In the meantime, I am working on the test.

Upchuk’s picture

Assigned: Upchuk » Unassigned
Issue tags: -Needs tests
FileSize
2.16 KB
6.55 KB

Here we go, test + fix.

The last submitted patch, 6: max_link_depth_test-2349071-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 6: max_link_depth-2349071-6.patch, failed testing.

Status: Needs work » Needs review

The last submitted patch, 6: max_link_depth_test-2349071-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 6: max_link_depth-2349071-6.patch, failed testing.

Status: Needs work » Needs review

The last submitted patch, 6: max_link_depth_test-2349071-6.patch, failed testing.

star-szr’s picture

Status: Needs review » Needs work

Thank you @Upchuk for the patch and test and sorry for the delay. Here's my review:

  1. +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
    @@ -356,8 +377,19 @@ public function save(array $form, FormStateInterface $form_state) {
    +      if ($parent_tree[$parent_id]->depth == 9) {
    +        $form_state->setErrorByName('menu_parent', $this->t("The parent menu link <em>@parent_link</em> has reached the maximum depth of 9.", array('@parent_link' => $form['menu_parent']['#options'][$form_state->getValue('menu_parent')])));
    

    I think it would be nice to get the value of 9 from the relevant class, because as far as I know it would be possible for an alternate menu storage to allow for a greater depth.

  2. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -597,6 +600,48 @@ function addInvalidMenuLink() {
    +        'weight[0][value]' => '0'
    ...
    +      'weight[0][value]' => '0'
    

    Minor: These should have a trailing comma per https://www.drupal.org/coding-standards#array.

Upchuk’s picture

Status: Needs work » Needs review
FileSize
2.3 KB
2.16 KB
6.63 KB

Thanks for the review.

I modified the conditional and retrieve the maximum depth from the menutree storage so that should be good I think. However, am not sure how to do it in the test. I don't know how I can retrieve the storage there to check it. So for now it's hardcoded to 9 in the test. If you can point me in the direction, I can look into it.

D

The last submitted patch, 17: max_link_depth_test-2349071-17.patch, failed testing.

star-szr’s picture

Awesome! I think from the test you should be able to do this:

$menu_link_tree = \Drupal::service('menu.link_tree');
$menu_link_tree->maxDepth();
star-szr’s picture

Status: Needs review » Needs work

So assuming I'm right the test needs a bit more work :) Thanks @Upchuk!

Upchuk’s picture

Status: Needs work » Needs review
FileSize
1.16 KB
2.37 KB
6.83 KB

Hey there,

Sorry it took so long but here we go.

D

The last submitted patch, 21: max_link_depth_test-2349071-21.patch, failed testing.

star-szr’s picture

Status: Needs review » Needs work

Thanks @Upchuk! Nitpicks alert:

  1. +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
    @@ -340,8 +361,19 @@ public function save(array $form, FormStateInterface $form_state) {
    +    // Check whether the selected parent has reached its maximum depth of 9.
    
    +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -442,6 +442,9 @@ function doMenuTests() {
    +    // Verify the maximum depth of 9 limitation.
    

    I think these should just say maximum depth instead of referring to 9.

  2. +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
    @@ -340,8 +361,19 @@ public function save(array $form, FormStateInterface $form_state) {
       protected function doValidate(array $form, FormStateInterface $form_state) {
    -    $extracted = $this->pathValidator->getUrlIfValid($form_state->getValue('url'));
    +    // Check whether the selected parent has reached its maximum depth of 9.
    +    $parent_id = strstr($form_state->getValue('menu_parent'), 'menu_link_content');
    +    if ($parent_id) {
    +      $parent_plugin = $this->menuLinkManager->getDefinition($parent_id);
    +      $menu_tree_params = new MenuTreeParameters();
    +      $menu_tree_params->setRoot($parent_plugin['id']);
    +      $parent_tree = $this->menuLinkTree->load($parent_plugin['menu_name'], $menu_tree_params);
    +      if ($parent_tree[$parent_id]->depth == $this->menuLinkTree->maxDepth()) {
    +        $form_state->setErrorByName('menu_parent', $this->t("The parent menu link <em>@parent_link</em> has reached the maximum depth of @depth.", array('@parent_link' => $form['menu_parent']['#options'][$form_state->getValue('menu_parent')], '@depth' => $this->menuLinkTree->maxDepth())));
    +      }
    +    }
     
    +    $extracted = $this->pathValidator->getUrlIfValid($form_state->getValue('url'));
         if (!$extracted) {
           $form_state->setErrorByName('url', $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $form_state->getValue('url'))));
         }
    

    Minor but adding the blank line underneath $extracted back would leave that part alone and probably make the patch easier to read too :)

  3. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -628,6 +631,52 @@ function addInvalidMenuLink() {
    +   * Attempts to add a menu link which exceeds the maxiumum depth.
    

    Minor: typo - maximum.

  4. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -628,6 +631,52 @@ function addInvalidMenuLink() {
    +  function addInvalidDepthMenuLink() {
    +
    +    $last_link = null;
    

    Minor, extra blank line here at the start of the method.

  5. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -628,6 +631,52 @@ function addInvalidMenuLink() {
    +    // Try to add a tenth level of menu links.
    

    Maybe something along the lines of "Try to add another level of menu links beyond the maximum."?

Upchuk’s picture

I don't really understand point 2 :) Can you elaborate a bit please?

Thanks!

D

Upchuk’s picture

Here you go.

D

Upchuk’s picture

Status: Needs work » Needs review

Starting the bot.

Upchuk’s picture

Removed trailing spaces.

D

Upchuk’s picture

Fixed reference to depth of 9 in test.

D

The last submitted patch, 27: max_link_depth-2349071-27.patch, failed testing.

The last submitted patch, 25: max_link_depth-2349071-25.patch, failed testing.

star-szr’s picture

Status: Needs review » Needs work

Thanks for your patience on this one @Upchuk :)

With that out of the way I have a couple thoughts:

  1. +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
    @@ -340,6 +361,18 @@ public function save(array $form, FormStateInterface $form_state) {
    +    $parent_id = strstr($form_state->getValue('menu_parent'), 'menu_link_content');
    +    if ($parent_id) {
    

    These lines look a bit suspect to me now, what if you tried to add a menu_link_content child menu link to a menu structure that already had a depth of 9 but where the parent link wasn't generated by the menu_link_content plugin? I think the validation needs to be flexible enough to handle that case otherwise it would still fail. This may also mean we're missing some test coverage for that case.

  2. +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
    @@ -340,6 +361,18 @@ public function save(array $form, FormStateInterface $form_state) {
    +        $form_state->setErrorByName('menu_parent', $this->t("The parent menu link <em>@parent_link</em> has reached the maximum depth of @depth.", array('@parent_link' => $form['menu_parent']['#options'][$form_state->getValue('menu_parent')], '@depth' => $this->menuLinkTree->maxDepth())));
    

    When manually testing I noticed that the error message contains the dashes that show depth, I think we probably don't want that. For example:

    The parent menu link ------------------ Yay has reached the maximum depth of 9.

Upchuk’s picture

Status: Needs work » Needs review
FileSize
1.58 KB

Hey there,

I've been looking more into this as we discussed on IRC and I created a patch that went the other direction: fixing what I think is a problem in the form element that shows the available menu parents.

I've noticed the following:

  protected function getParentDepthLimit($id) {
    if ($id) {
      $limit = $this->menuLinkTree->maxDepth() - $this->menuLinkTree->getSubtreeHeight($id);
    }
    else {
      $limit = $this->menuLinkTree->maxDepth() - 1;
    }
    return $limit;
  }

This function is supposedly responsible for checking whether an ID is passed and returning the appropriate depthLimit. For the cases where the menu link exists, that is fine, the return will be max - the subtree. But for new items, it should be maxDepth - 1 (which is actually what this code should do). The problem is an ID is passed even if the item is new (an ID is generated on the fly in the menu link content entity) so $id is always populated. This means the max depth is always 9 which is too much.

My patch tries to load the plugin definition first before based on the ID to make sure a link actually exists. For that, I had to also create a bridge to the treeStorage in the MenuLinkTree class (hope that's ok).

Let me know if this is a good approach and I'll work on the test.

pwolanin’s picture

So, as an alternative can we pass in an empty string for a new link?

pwolanin’s picture

Issue tags: -Amsterdam2014 +Needs tests
FileSize
1.06 KB

This this?

star-szr’s picture

Thanks @pwolanin! That looks promising, and for tests I think they could be built on top of the tests in #28.

I just tested D7 HEAD and the depth is limited on the add menu link form, not just the edit menu link form.

Upchuk’s picture

Assigned: Unassigned » Upchuk
Status: Needs review » Needs work

Thanks @Cottser. I just tested @pwolanin's patch and works.

Working on the test now.

D

Upchuk’s picture

Status: Needs work » Needs review
FileSize
1.89 KB
1.89 KB
2.96 KB

Here we go. Interdiff, test, and test+fix.

D

The last submitted patch, 37: 2349071-test-37.patch, failed testing.

star-szr’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests

Nice, looking good to me! This is the only thing that jumped out at me, just a docs/coding standard thing:

+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -628,6 +631,39 @@ function addInvalidMenuLink() {
   /**
+   * Checks to see if new menu links can be added to parents who have reached the
+   * maximum depth.
+   */

This summary line needs to fit in 80 characters per https://www.drupal.org/node/1354#drupal.

Here's a suggestion: "Tests that parent options are limited by depth when adding menu links."

Upchuk’s picture

FileSize
551 bytes
2.93 KB

Hey there,

Here you have the update :)

D

Upchuk’s picture

Status: Needs work » Needs review

Starting the bot :)

pwolanin’s picture

The test seems a little non-specific and fragile. Can you actually target the selection option? or directly call the API instead of looking at a form page?

Upchuk’s picture

FileSize
573 bytes
1.96 KB
3.02 KB

Here we go, as discussed, using assertNoOption() in the test.

The last submitted patch, 43: 2349071-test-43.patch, failed testing.

pwolanin’s picture

Status: Needs review » Needs work

minor: $maxDepth

normal variables should be snake case like $max_depth

Also, you might as well assert that each expected option *is* present?

Upchuk’s picture

Will change the var name but is there a reason why the expected options should be tested here? Are they not tested elsewhere? This test deals with the *wrong* option :)

pwolanin’s picture

Issue tags: +D8 Accelerate NJ

@Upchuk - sure, it's not elsewhere - maybe it should be, but I'm guessing we don't have much explicit coverage of this form element.

Upchuk’s picture

Status: Needs work » Needs review
FileSize
1.85 KB
2.46 KB
3.52 KB

Here we go :)

Status: Needs review » Needs work

The last submitted patch, 48: 2349071-48.patch, failed testing.

Status: Needs work » Needs review

Upchuk queued 48: 2349071-48.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 48: 2349071-48.patch, failed testing.

Upchuk’s picture

FileSize
3.56 KB

Here is the "failed to apply" patch rerolled.

D

The last submitted patch, 48: 2349071-test-48.patch, failed testing.

Upchuk’s picture

Status: Needs work » Needs review

starting bot.

Status: Needs review » Needs work

The last submitted patch, 52: 2349071-52.patch, failed testing.

pwolanin’s picture

Fatal error: Call to a member function getPluginId() on a non-object in /var/lib/drupaltestbot/sites/default/files/checkout/core/modules/menu_ui/src/Tests/MenuTest.php on line 680
FATAL Drupal\menu_ui\Tests\MenuTest: test runner returned a non-zero error code (255).

Upchuk’s picture

Status: Needs work » Needs review
FileSize
2.47 KB
3.57 KB

Fixed. Had to account for the changes in #2416955: Convert MenuLinkContent to use a link widget.

PS what is the D8 Accelerate NJ tag for here?

D

The last submitted patch, 57: 2349071-test-57.patch, failed testing.

star-szr’s picture

Glad to see this moving :) I would mention we're not supposed to use t() in assert messages:

+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -648,6 +651,48 @@ function addInvalidMenuLink() {
+    $this->assertNoOption('edit-menu-parent', $value, t('The invalid option is not there.'));
...
+      $this->assertOption('edit-menu-parent', $link, t('The valid option number @number is there.', array('@number' => $key + 1)));

https://www.drupal.org/simpletest-tutorial-drupal7#t

pwolanin’s picture

Tag is for issues we worked on related to menu links/routing during this sprint: https://groups.drupal.org/node/453848

Upchuk’s picture

FileSize
1.72 KB
3.55 KB

Hey there guys,

Thanks @Cottser, you are right. I made the change.

Can we get this RTBC'ed? :)

star-szr’s picture

Status: Needs review » Needs work

I'm happy to sign off on this, just a couple more small docs/standards things:

  1. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -648,6 +651,48 @@ function addInvalidMenuLink() {
    +    // Check that the last link cannot be selected as parent in the new menu link form.
    ...
    +    // Check that all links but the last can be selected as parent in the new menu link form.
    

    Please wrap these at 80 characters per https://www.drupal.org/node/1354#drupal.

  2. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -648,6 +651,48 @@ function addInvalidMenuLink() {
    +    $this->assertNoOption('edit-menu-parent', $value, 'The invalid option is not there');
    

    I can't find a source but I think assertion messages need to end in a period too.

Upchuk’s picture

Status: Needs work » Needs review
FileSize
1.16 KB
3.51 KB

Here you go.

D

star-szr’s picture

Status: Needs review » Reviewed & tested by the community

Just for future reference those >80 char comments could be wrapped rather than truncated/reworded :)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue is a normal bug fix, and doesn't include any disruptive changes, so it is allowed per https://www.drupal.org/core/beta-changes. Committed ea8d1aa and pushed to 8.0.x. Thanks!

diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php
index 14b9219..8d574f4 100644
--- a/core/modules/menu_ui/src/Tests/MenuTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -674,7 +674,7 @@ function checkInvalidParentMenuLinks() {
         'expanded[value]' => FALSE,
         'weight[0][value]' => '0',
       );
-      $test = $this->drupalPostForm("admin/structure/menu/manage/{$this->menu->id()}/add", $edit, t('Save'));
+      $this->drupalPostForm("admin/structure/menu/manage/{$this->menu->id()}/add", $edit, t('Save'));
       $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $title));
       $last_link = reset($menu_links);
       $created_links[]  = 'tools:' . $last_link->getPluginId();

Removed unused variable on commit.

  • alexpott committed ea8d1aa on 8.0.x
    Issue #2349071 by Upchuk, Cottser, pwolanin: EntityStorageException when...

Status: Fixed » Closed (fixed)

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