Recently I came across an obscure bug: http://drupal.org/node/179465 Basically it's caused by Statistics.module passing incorrect data in hook_menu(), namely creating a secondary tab under a non-existing parent (under some conditions). This bug itself is discussed in that other issue, and is quite simple in fact.

But the weird behavior resulting from such a simple mistake is scary - consider: A collision between Statistics and (disabled!) Tracker modules disrupts menu system so badly, that User-editing page doesn't show Profile-defined secondary tabs, making the profiles useless. And the broken tab itself is rendered nicely and works without problems! Now try to track such bugs down - luckily it took me "only" one day, but I'm seriously scared of similar things upcoming in contrib (unlucky combinations of modules disrupting other, unrelated ones). How would THAT look like, support-wise? :-(

Details: The exact machanism of above-linked bug is following (only happens with mentioned core modules until that other patch lands):
- Statistics define a tab 'user/%/track/navigation'.
- Tracker is disabled, so the parent 'user/%/track' doesn't exist.
- _menu_router_build() notice the missing parent, and "helpfully" reparent the tab to 'user/%'.
- On page request for 'user/xx/edit/category', the menu_local_tasks() function iterates through all tabs, presuming that tabs of the same level have equally-long paths:
* First secondary tabs on 'user/%/edit/***' are rendered and stored into array, with the key = number of path elements being 4.
* Then it go to parent-level and render primary tabs for 'user/%/***', whose number of path elements is generally 3, excepting the re-parented entry 'user/%/track/navigation' having 4 elements. Unluckily, that entry goes last, so the number 4 remains in variables, and is later used as array key for storing primary tabs. Thus, previously rendered secondary tabs are OVERWRITTEN by primary tabs.
* The resulting array (only one element in this case) is re-sorted to gain zero-starting keys, so primary tabs appear correctly as element #0. Secondary tabs are gone.

IMO, the menu system needs some simple sanity-check, to avoid such situations. The attached patch changes _menu_router_build() to discard broken parent-missing entries rather than re-parenting them. This prevents wrong data from entering the menu system, thus avoiding problems, and also forcing module-developer to fix the broken item (because it doesn't show up in menu unless correct). Also note, that the new code (very minimal, but still...) runs only at menu rebuild, not runtime.

Things to consider / discuss:

- Is the approach correct? Is menu entry with missing parent ALWAYS a wrong thing? Looking at http://api.drupal.org/api/group/menu/6 , I can't see whether it's ILLEGAL or only just USELESS to define menu items like 'a', 'a/b' and 'a/b/c/d' (note that 'a/b/c' is missing!)

- Can someone clarify what's the variable_set('menu_masks', $masks) all about, and whether it needs to be modified too? This one I somehow can't understand.

- Probably it would be good to have another sanity-check, to discard tab-sets with no default tab? Looking at the code, I can't imagine how exactly it might disrupt menu_local_tasks(), but I guess it might. But my knowledge of menu system is insufficient to try implementation of this part.

- Another approach might be a little bigger rewrite of menu_local_tasks(), to make it foolproof, not relying on path-lengths for array-keying, and so impossible to be fooled this way. But this means more work, and definitely goes beyond my limited knowledge of menu system. Still - might it be better for some reason?

- Any other ideas from menu-gurus, particularly possible side-effects, if that's the case... ;-)

Comments

webchick’s picture

Category: task » bug
Priority: Normal » Critical

I would call this a critical bug. We need an alternative to the workaround proposed at http://drupal.org/node/179465, which is going to be unworkable in contrib.

webchick’s picture

For reference, the way the 5.x menu system seems to handle this condition is:

* You can define menu paths wherever you darn well please, regardless if the parent is there or not.
* If a user ever attempts to directly access a non-existent child (such as user/1/track), it will redirect them to the nearest existing parent (user/1).

chx’s picture

Status: Needs review » Needs work

I would rather see the tab render code fixed than see the reparent code removed.

JirkaRybka’s picture

Title: Sanity check for hook_menu() » Tabs rendering broken if non-existing item is in path

I thought it over again in less hurry, and decided to try and provide another patch. Now that webchick pointed out that CVS module define tabs the same way, I see that the problem is much more widespread, and so parentless tabs were probably seen as allowed in the past, although documentation doesn't say anything (needs update BTW).

That said, it's menu_local_tasks() what needs a fix, and I think I know how now. Expect another patch from me within hours...

chx’s picture

Status: Needs work » Needs review
StatusFileSize
new3.68 KB

when router and links got split, depth was lost.

chx’s picture

StatusFileSize
new8.58 KB

Screenshot.

JirkaRybka’s picture

StatusFileSize
new1.47 KB

Thanks chx - seems we were working on the same simultaneously.

I just created another patch, more simple: All we are interested about is relative order, so we don't really need to have the depth for this - a simple counter would be enough. Attaching my patch.

chx’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new1.52 KB

For the first loop, the next path is always picked from the children of the path so yes then the depth is growing indeed. The second loop picks the next path from the parents so depth is indeed decreasing. Very nice solution which does not need a schema change. I merely changed $key to $depth and rolled the patch as a standard Drupal patch, please do not credit me for it.

JirkaRybka’s picture

Status: Reviewed & tested by the community » Needs review

chx - Your patch works fine, resulting tabs are identical (correct) for both patches.

But my preference is leaned towards my one, because it's much smaller change, only just local thing inside menu_local_tasks() without any change to structures outside. Your patch in the other hand changes a bit more, especially adds a new column to the database - and that's bad now (post-beta1).

Now, we need other's review! :)

JirkaRybka’s picture

Status: Needs review » Reviewed & tested by the community

Accidentally posted over your reply... So correcting status back to RTBC. The #8 version changes only just a variable name which is OK with me, so I agree this is RTBC indeed. :)

pwolanin’s picture

Should 10 and 9 really be bare int contstants? if 10 == MENU_MAX_DEPTH + 1 then it should be coded that way. (thanks to webernet for pointing this out)

pwolanin’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new1.72 KB

attached patch changes the explicit int values to do be defined in terms of MENU_MAX_DEPTH and revises the code comment a little. Though I suppose we could just as well use 1000 + MENU_MAX_DEPTH as the starting depth?

pwolanin’s picture

actually is MENU_MAX_DEPTH really the right thing? I think MENU_MAX_PARTS is actually the relevant constant?

pwolanin’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new1.7 KB

chx clarified for me - the value does not matter. So let's use 1 and 0 as non-confusing starting depth values (though 10 and 9 are fine too). Moving back to RTBC per above comments.

JirkaRybka’s picture

Status: Reviewed & tested by the community » Needs review

I used the integers 10 and 9 only just as relative temporary numbers, the keys will go away anyway, that's true.

I used higher integers than 0 and 1, to stay off negative array-key integers, which might otherwise occur if decreasing the zero integer. The reason was the last comment (about ksort() and negative array keys) at this page: http://cz.php.net/manual/en/function.ksort.php According to that comment, there's a suspicion that ksort() might 'yields some odd results' if negative keys used.

So this needs to be checked, probably for different supported PHP-versions too, if we want to change 10/9 to 1/0! Personally, I just used higher values to avoid this danger, because the numbers really doesn't matter.

pwolanin’s picture

so, it may be a good idea to use a larger number, but why not start at 100 or 1000 if it's arbitrary? Note that in theory, there is no limit now to the depth of tabs, so 10 may not be conservative enough. In any case to avoid future confusion this needs a small code comment.

pwolanin’s picture

StatusFileSize
new1.71 KB

ok, here's a re-roll with a 1000 offset and reworked the code comment to mention it.

JirkaRybka’s picture

Status: Needs review » Reviewed & tested by the community

Fine with me. Tested again after this minor change: Applies smoothly and works well.

Setting back to RTBC.

gábor hojtsy’s picture

Status: Reviewed & tested by the community » Fixed

Great, thanks, committed. If this fixes the problems in the other issue, please close that one too.

Anonymous’s picture

Status: Fixed » Closed (fixed)