I'm trying to fix up Path Access a little, as it's currently doing its access checking in hook_init() which is causing problems. hook_menu_alter is clearly a better place. So, I need to chain its access function onto every single menu item. I'm tring to do this using the following code:

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $key => $value) {
    chain_menu_access_chain($items[$key], '_path_access_check_permission');
  }
}

I'm running into some issues though. I have set my access callback to simply return TRUE so I know that is not the casue of the issues. Here are some of the issues I get.

1) Visit a node view page

Error message

Warning: Missing argument 1 for user_access() in user_access() (line 778 of /Users/tom/workspace/alumni/modules/user/user.module).
Notice: Undefined variable: string in user_access() (line 808 of /Users/tom/workspace/alumni/modules/user/user.module).
Warning: Missing argument 1 for user_access() in user_access() (line 778 of /Users/tom/workspace/alumni/modules/user/user.module).
Notice: Undefined variable: string in user_access() (line 808 of /Users/tom/workspace/alumni/modules/user/user.module).

2) Menu items that point to Empty Page no longer show up, and access is denied to those pages.

3) Any page that I view whilst logged in show the following error repeated numerous time. The pages display ok though other than that.

Warning: Missing argument 1 for user_access() in user_access() (line 778 of /Users/tom/workspace/alumni/modules/user/user.module).

Comments

mrfelton’s picture

If I pass TRUE as the 4th paramater to the function, it seems to work better:

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $key => $value) {
    chain_menu_access_chain($items[$key], '_path_access_check_permission', array(), TRUE);
  }
}

I still get the "Warning: Missing argument 1 for user_access() in user_access() (line 778 of /Users/tom/workspace/alumni/modules/user/user.module)." warnings when logged in

mrfelton’s picture

After a little bit of debugging, I believe the problem is on those menu items that define 'access callback' => TRUE,

mrfelton’s picture

Status: Active » Needs work
StatusFileSize
new1.29 KB

Attached patch gets things working properly for the menu paths defined as 'access callback' => TRUE,, but I still get all the warnings when logged on.

mrfelton’s picture

I believe that the issue is that you are forcing a call to the old callback. This defaults to user_access(). user_access() requires a value for the first argument. However, many contrib modules (and also some core modules including block, filter, image, menu, node, path and system) do not specify any 'access arguments' for some of their callback paths.

Somehow, the core obviously bother to call user_access() if a module has not defined any 'access arguments'. I think this module should work in the same way - if the access callback is user_access, and no arguments were set for it, it should not get called or should default to TRUE.

mrfelton’s picture

Status: Needs work » Needs review

I got around this problem by altering my code to the following:

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $key => $value) {
    if (isset($value['type']) && $value['type'] != MENU_DEFAULT_LOCAL_TASK) {
      chain_menu_access_chain($items[$key], '_path_access_check_permission', array(), FALSE);
    }
  }
}

So I don't bother applying our access check to default paths, since thee inherit the access checks of their parent paths anyway. This problem of breakage of default menu local tabs is actually documented in your module by CHX already. I don't think any other changes are needed in chain_menu_access other than the one in my patch at #3.

salvis’s picture

Applying CMA to every menu item — that's a very interesting stress test, thank you for your feedback!

Your patch in #3 makes perfect sense.

What do you mean in #4 with

Somehow, the core obviously bother to call user_access() if a module has not defined any 'access arguments'.

? Does core grant or deny access if neither 'access callback' nor 'access arguments' is specified?

Re. #4: The normalization

  $item += array('access callback' => 'user_access', 'access arguments' => array());

fails if it sets both items. We should probably do something like

  $item += array(
    'access callback' => (isset($item['access arguments']) ? 'user_access' : TRUE),
    'access arguments' => array(),
  );

instead.

Your work-around in #5 means that you're not chaining items that don't set 'type'. Is that the intended behavior? (I don't know if there are any such menu items.)

Do you know what core does if you specify a different access for a MENU_DEFAULT_LOCAL_TASK than for its parent?

mrfelton’s picture

After more investigation and lots of debugging, I found that I needed to call menu_alter like this. Read the code comment for details.

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $path => &$item) {
    // Default menu items don't necessarily have access arguments explicitly defined,
    // and the default access callback is user_access which requires an access argument.
    // In core, if access arguments have not been set, they are set to those of the parent
    // item in _menu_router_build. We use a similar logic here to ensure that the access
    // arguments are set to those of the parent if they have not been explicitly defined
    // on the item. This ensure that chain_menu_access_chain functions correctly (mrfelton).
    if (isset($item['type']) && ($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback'])) {
      $parts = explode('/', $path, MENU_MAX_PARTS);
      $number_parts = count($parts);
      $parent_path = implode('/', array_slice($parts, 0, $number_parts-1));
      $parent = menu_get_item($parent_path);
      if (isset($parent['access_callback'])) {
        $item['access callback'] = $parent['access_callback'];
      }
      if (!isset($item['access arguments']) && isset($parent['access_arguments'])) {
        $item['access arguments'] = menu_unserialize($parent['access_arguments'], $parts);
      }
    }
    chain_menu_access_chain($item, '_path_access_check_permission');
  }
}

So, core actually sets the access arguments to those of the parent item if they have not been set on the default menu item. I couldn't find a way to do this in chain_menu_access without radically altering the API, since you need to be able to get details of the menu's parent item in order to set the access arguments correctly.

salvis’s picture

How is that better than just leaving the MENU_DEFAULT_LOCAL_TASKs alone and letting core do the inheriting? Why do you need to chain the default children at all?

Core doesn't just set the access arguments (as your comment seems to say) but also the access callback of the default child, does it not?

So what does core do, if the parent and the default child have different access?

mrfelton’s picture

If I don't call my access function on the MENU_DEFAULT_LOCAL_TASKs, then paths like node/% whose MENU_DEFAULT_LOCAL_TASK is node/%/view will not get checked properly.

Yes, core does set the access callback as well as the access arguments to that of the parent - as does my code (I think I just forgot to comment that part of it).

If the parent and default child have different access, then Core will use what ever has been defined. There is a special case in core (it's even referred to as a special case in the core docs) where there is supposed to be a default behavior for MENU_DEFAULT_LOCAL_TASKs to default to the access callback and arguments to those of the parent item if they have not been defined on the child.. I don't know the reasoning for this - I guess it saves module authors a few lines of code here and there.

To use this API, you have to call chain_menu_access_chain($item, '_path_access_check_permission'); from hook_menu_alter(). At this point, $item does not have access arguments set for default MENU_DEFAULT_LOCAL_TASKS unless they have been explicitly set by the defining module - which most of the time they have not. So, in this case chain_menu_access will call user_access without any arguments, and that's a problem. So, what I've done is ensure that the MENU_DEFAULT_LOCAL_TASKS with no explicitly defined access callback or arguments get them set in hook_menu_access, thus passing them onto chain_menu_access_chain, thus resolving the issue.

mrfelton’s picture

Look at includes/menu.inc around line 3509 - that is where it does it's magic.

// If an access callback is not found for a default local task we use
// the callback from the parent, since we expect them to be identical.
// In all other cases, the access parameters must be specified.

mrfelton’s picture

Same thing, but slightly refined.

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $path => &$item) {
    // Default menu items don't necessarily have access arguments explicitly defined,
    // and the default access callback is user_access which requires an access argument.
    // In core, if access arguments have not been set, they are set to those of the parent
    // item in _menu_router_build. We use a similar logic here to ensure that the access
    // callback and arguments are set to those of the parent if they have not been explicitly
    // defined on the item. This ensure that chain_menu_access_chain functions correctly
    // (mrfelton).
    if (isset($item['type']) && ($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback'])) {
      $parts = explode('/', $path, MENU_MAX_PARTS);
      $number_parts = count($parts);
      $parent_path = implode('/', array_slice($parts, 0, $number_parts-1));
      if (isset($items[$parent_path]) && $parent = $items[$parent_path]) {
        if (isset($parent['access callback'])) {
          $item['access callback'] = $parent['access callback'];
        }
        if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
          $item['access arguments'] = $parent['access arguments'];
        }
      }
    }
    chain_menu_access_chain($item, '_path_access_check_permission');
  }
}

Realised that there was no need to call menu_get_item() to get the parent item, since we already have the information in the $items array.

salvis’s picture

If the parent and default child have different access, then Core will use what ever has been defined. There is a special case in core (it's even referred to as a special case in the core docs) where there is supposed to be a default behavior for MENU_DEFAULT_LOCAL_TASKs to default to the access callback and arguments to those of the parent item if they have not been defined on the child.. I don't know the reasoning for this - I guess it saves module authors a few lines of code here and there.

Very interesting. I'd say it's actually a bug if the access of MENU_DEFAULT_LOCAL_TASKs is specified rather than inherited. Since node/%/view shows the same thing as node/%, its access should also be the same, and the best way to ensure this is to inherit it.

So, I still think it would be best for you to leave the MENU_DEFAULT_LOCAL_TASKs alone and let them default to their parent. Especially if you're a good citizen and keep in mind that some other module might come after yours and expect to find things as they normally are in core. Something like

/**
 * Implements hook_menu_alter().
 */
function path_access_menu_alter(&$items) {
  foreach ($items as $key => $value) {
    if (!isset($value['type']) || $value['type'] != MENU_DEFAULT_LOCAL_TASK) {
      chain_menu_access_chain($items[$key], '_path_access_check_permission');
    }
  }
}

which is close to what you had in #5, except for a slight variation of the condition.

I'm beginning to think that chain_menu_access_chain() should refuse to do anything on MENU_DEFAULT_LOCAL_TASK items and return FALSE.

mrfelton’s picture

I would have said that it's a bug in core since an access argument is a required paramater to user_access, and these MENU_DEFAULT_LOCAL_TASK items aren't setting any access arguments. However, Core goes out of its way to set the access callback and arguments to those of the parent if they have not been manually set. So really there is no bug. It's strange default behavior of core, but it's been like that in core since the early days. This behavior is documented in hook_menu in core:

"access callback": A function returning TRUE if the user has access rights to this menu item, and FALSE if not. It can also be a boolean constant instead of a function, and you can also use numeric values (will be cast to boolean). Defaults to user_access() unless a value is inherited from the parent menu item; only MENU_DEFAULT_LOCAL_TASK items can inherit access callbacks. To use the user_access() default callback, you must specify the permission to check as 'access arguments' (see below).

"access arguments": An array of arguments to pass to the access callback function, with path component substitution as described above. If the access callback is inherited (see above), the access arguments will be inherited with it, unless overridden in the child menu item.

I haven't tested this, but reading the code in core suggests to me that it is possible to have a MENU_DEFAULT_LOCAL_TASK with a different access callback and arguments to it's parent. See the following from _menu_router_build():

        // If an access callback is not found for a default local task we use
        // the callback from the parent, since we expect them to be identical.
        // In all other cases, the access parameters must be specified.
        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
          $item['access callback'] = $parent['access callback'];
          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
            $item['access arguments'] = $parent['access arguments'];
          }
        }

So the inherited defaults only get applied to children if they have not been manually set on the child. There is nothing in the documentation that states the two must be the same. The parent and child are different paths at the end of the day. /admin/content & /admin/content/list - its just that generally they are made to be identical through inheritance as that is the most logical thing in 99.9% of cases (I can't really think of case where it would make sense to do something else though).

Your code in #12 wont work, because the path node/%/view will end up not being protected as it's a MENU_DEFAULT_LOCAL_TASK. This is the problem that let me to the solution in #11 in the first place.

mrfelton’s picture

hook_menu_alter() is called from menu_router_build(). It runs right before _menu_router_build() which is where core does the default inheritance thing:

  // Alter the menu as defined in modules, keys are like user/%user.
  drupal_alter('menu', $callbacks);
  list($menu, $masks) = _menu_router_build($callbacks);

So, at the point where your code is running, from within hook_menu_alter(), _menu_router_build() has not yet been called and so the default access callback and arguments have not yet been assigned.

salvis’s picture

If 'type' is not specified, it defaults to MENU_NORMAL_ITEM, i.e. the condition in #12 is correct rather than the one in #5. However, you're right, #12 only works if the access of the MENU_DEFAULT_LOCAL_TASK item is fully inherited, and otherwise your module would miss the item.

Core does support having different access on MENU_DEFAULT_LOCAL_TASKs vs. their parents, either different 'access callback' and 'access arguments' or just different 'access callback', even though that doesn't really make sense from the user perspective.

CMA should provide the same support that core does:

  1. If we chain a MENU_DEFAULT_LOCAL_TASK child and ...
    1. it does not specify its own access, then we need to copy the parent item's access before chaining onto it, otherwise we lose that information.
    2. it specifies only 'access arguments', then we need to copy the parent item's 'access callback' before chaining onto it, otherwise we risk calling the wrong function with the right arguments.
    3. it specifies both 'access callback' and 'access arguments': we can chain normally.
  2. We have to worry about the parent items, too! If we chain a parent item and...
    1. the MENU_DEFAULT_LOCAL_TASK child does not specify its own access, then the child can continue to inherit its parent's access; all is well, nothing to do.
    2. the MENU_DEFAULT_LOCAL_TASK child specifies only 'access arguments', then we're breaking the child item right there. This means we must look for such a child and copy the parent's 'access callback' to it before chaining the parent.
    3. the MENU_DEFAULT_LOCAL_TASK child specifies both 'access callback' and 'access arguments': we could try to check whether they are the same as the parent's, but it's probably best to treat them as disconnected, just chain the parent, and let the caller worry about the situation.
      However, this means that the caller can easily be surprised, if he expects the situation as it's in core, but a module such as yours has run before him. [Your module should probably try to run last!] I'm not sure we can solve this problem...
  3. There's a challenge here: calling menu_get_item() from hook_menu_alter() is probably not a good idea, which means we can't get to the parent/child items. We'll need to have the entire menu array passed to us from the caller.

It shouldn't be the CMA client's responsibility to worry about these things. The reason for having CMA is that we implement this once and for all, so that it just works.

Did I miss something?

mrfelton’s picture

I think you are spot on.

Calling menu_get_item() from within hook_menu_alter() wont actually work properly anyway. At I couldn't get it to work properly when I was doing that in #7. It worked for 90% of cases, but not for those items that were relying on the default inheritance. It's much better to get the parent information out of the $items arraw that we already have in hook_menu_alter() for performance reasons if nothing else.

My original plan was to modify CMA so that the entire $items array, or the current $item + the parent menu item's path could be passed in as arguments, but I was afraid to make such heavy modifications to the API. But, you are absolutely right. This should be the responsibility of CMA, not the client.

ps. My module does run last (weight 1001). It has to, since otherwise it's not possible to modify paths that have been added dynamically through hook_menu_alter(), like Views page display paths for example.

chx’s picture

So the only thing from a MENU_DEFAULT_LOCAL_TASK definition should be used is the weight and the title. I can see Drupal uses the access control when drawing the tab itself but that's plain wrong because the href points to the parent and so the parent's access control should be used. That smells like a core bug to me.

Nonetheless, the simplest thing to do is to never set access callback on MENU_DEFAULT_LOCAL_TASK and let core handle it. As per the comment in chain_menu_access_chain , it probably breaks on default tabs -- if that's a problem we can add a check into chain_menu_access_chain so that MENU_DEFAULT_LOCAL_TASK is never altered. Now.

function _menu_check_access(&$item, $map) {
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
  // Check for a TRUE or FALSE value.
  if (is_numeric($callback)) {
    $item['access'] = (bool) $callback;
  }

vs

function _chain_menu_access_callback_call($access_callback, $access_arguments) {
  if (is_numeric($access_callback)) {
    // It's a number (see hook_menu()).
    return (bool) $access_callback;
  }

that's a clear-cut bug, we need $access_callback = empty($access_callback) ? 0 : trim($access_callback); in here to match core.

salvis’s picture

The plan is for CMA to refuse to chain MENU_DEFAULT_LOCAL_TASK items. They should never specify their own access and always inherit from their parent item.

@mrfelton:

Your code in #12 wont work, because the path node/%/view will end up not being protected as it's a MENU_DEFAULT_LOCAL_TASK. This is the problem that let me to the solution in #11 in the first place.

Why do you write this? According to your #14, node/%/view will come in without any access specification, and if we keep it that way, it will subsequently (in _menu_router_build()) get assigned the access specification of node/%, which you've protected.

@chx:

Regarding the access check in menu_local_tasks(): I think the items have been passed through _menu_router_build(), which has applied the inheritance by copying the parent's access specification to the default tab item. Thus it should be OK to check the child's access.

salvis’s picture

StatusFileSize
new3.04 KB

Please try this out and let us know how it goes, without chaining the MENU_DEFAULT_LOCAL_TASK items.

chx’s picture

salvis' patch is correct , it's mostly copy - paste out of menu.inc (router build and menu check access).

salvis’s picture

@chx: Thank you for your help with this (also behind the scenes)!

So the only thing from a MENU_DEFAULT_LOCAL_TASK definition should be used is the weight and the title.

Should we try to add this to the hook_menu() documentation?

mrfelton’s picture

From my initial testing, the patch at #19 seems to do the trick.

salvis’s picture

Title: Unable to chain infront of some system paths » Unable to chain in front of some system paths
Version: 7.x-1.x-dev » 6.x-1.x-dev
Status: Needs review » Patch (to be ported)

Thanks, I've committed this to D7 and created BETA3.

@chx: We don't have exceptions in D6. What should we do? Return FALSE?

http://drupal.org/project/usage/chain_menu_access shows that there's little interest in the D6 version, but I'd still like to keep it current.

mrfelton’s picture

Status: Patch (to be ported) » Active

Hmm. I don't think I properly updated from git when testing previously. There are still some issues with this when trying to do the following

function path_access_menu_alter(&$items) {
  foreach ($items as $path => &$item) {
    chain_menu_access_chain($item, '_path_access_check_permission');
  }
}

I get repeated error messages saying:

Warning: Missing argument 1 for user_access() in user_access() (line 778 of /Users/tom/workspace/alumni/modules/user/user.module).

This is the exact same problem as wehn I originally reported this problem.

mrfelton’s picture

Status: Active » Patch (to be ported)

Sorry - ignore that. path_Access module has some erroneous code. Everything looks good with CMA.

mrfelton’s picture

Found a problem with this.

I can't successfully chain an access callback to the path admin/structure/types/manage/%node_type/delete

It's related to the following in chain_menu_access_chain()

  // Normalize the menu router item.
  if (!isset($item['access callback']) && isset($item['access arguments'])) {
    // Default callback.
    $item['access callback'] = 'user_access';
  }
  if (!isset($item['access callback']) || empty($item['page callback'])) {
    $item['access callback'] = 0;
  }

The path in question is configured as:

  $items['admin/structure/types/manage/%node_type/delete'] = array(
    'title' => 'Delete',
    'page arguments' => array('node_type_delete_confirm', 4),
    'access arguments' => array('administer content types'),
    'file' => 'content_types.inc',
  );

So this path has no access callback defined (since it should default to user_access and it has access arguments set. This is correct and so the first condition matches and access callback is set to user_access.

But then, because it has no page callback defined, the second condition also matches and so access callback is then set to 0 - which is wrong.

What is the logic behind this second condition?

  if (!isset($item['access callback']) || empty($item['page callback'])) {
    $item['access callback'] = 0;
  }

Drupal core defaults the page callback to that of the parent menu item before running that test. chain_menu_access doesn't. Herein lies the problem.

salvis’s picture

Status: Patch (to be ported) » Needs work

I'm not sure why chx put the second condition there, but I thought it wouldn't hurt.

@chx: Can we remove it?

(I'm on the road and may only be able to take care of this in 10 days.)

mrfelton’s picture

Same issue with admin/modules/list/confirm

Defined as:

  $items['admin/modules/list/confirm'] = array(
    'title' => 'List',
    'access arguments' => array('administer modules'),
    'type' => MENU_VISIBLE_IN_BREADCRUMB,
  );

Works if I change system.module to:

  $items['admin/modules/list/confirm'] = array(
    'title' => 'List',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('system_modules'),
    'access arguments' => array('administer modules'),
    'type' => MENU_VISIBLE_IN_BREADCRUMB,
  );

Again, core allows this menu item to inherit the page callback and page arguments from its parent menu item, even though its not a MENU_DEFAULT_LOCAL_TASK. In fact, this menu item is actually inheriting these properties form the parents parent 'admin/modules'. Is this a core bug or expected behavior?

In menu.inc it says:

        // If an access callback is not found for a default local task we use
        // the callback from the parent, since we expect them to be identical.
        // In all other cases, the access parameters must be specified.
        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
          $item['access callback'] = $parent['access callback'];
          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
            $item['access arguments'] = $parent['access arguments'];
          }
        }
        // Same for page callbacks.
        if (!isset($item['page callback']) && isset($parent['page callback'])) {
          $item['page callback'] = $parent['page callback'];
..

It says that the access callback should be inherited for the default local task. It says that the same should be true for page callbacks, yet there is no restriction in this happening only for MENU_DEFAULT_LOCAL_TASK as there is with the access callback.

chx’s picture

Sure, I removed it (because salvis travels didnt want to hold you up with it).

salvis’s picture

Status: Needs work » Patch (to be ported)

Thank you chx, I just found (actually working!) Internet access at the place where I'm staying for most of the week.

@mrfelton: Please open a new issue (linking to this one would be helpful) if another problem pops up.

mrfelton’s picture

Yep, thanks. Things seem to be working smoothly now.

salvis’s picture

Good to know — I appreciate your help with getting this to work universally.

salvis’s picture

Status: Patch (to be ported) » Fixed

Fixed and released BETA4 for D7 and D6.

Status: Fixed » Closed (fixed)

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

yched’s picture

There are still issues with MENU_DEFAULT_LOCAL_TASK with the current code, when the parent menu item uses chain_menu_access_chain() :

If the default task is defined as

$items['foo/bar'] = array(
  'title' => $title,
  'page callback' => 'my_callback',
  'access arguments' => array('administer content types'),
  'type' => MENU_DEFAULT_LOCAL_TASK,
);

- No 'access callback', so _menu_router_build() will use the one from the parent, which has been altered to '_chain_menu_access_callback'.
- But there are explicit 'access arguments', so _menu_router_build() keeps them and doesn't override from the parent 'access arguments' (which have been adapted for _chain_menu_access_callback()).
- So when checking access for 'foo/bar', _menu_check_access() calls _chain_menu_access_callback('administer content types'). Strange stuff ensues, since that's not the kind of arguments that the callback expects.

[edit : using 6.x-1.0-beta4. My use case is "alter all existing menu items" as well]

salvis’s picture

Status: Closed (fixed) » Active

It is an error to specify access arguments (and page callback for that matter) on the MENU_DEFAULT_LOCAL_TASK.

Look at the example:

// Make "Foo settings" appear on the admin Config page
$items['admin/config/foo'] = array(
  'title' => 'Foo settings',
  'type' => MENU_NORMAL_ITEM,
  // Page callback, etc. need to be added here.
);
// Make "Global settings" the main tab on the "Foo settings" page
$items['admin/config/foo/global'] = array(
  'title' => 'Global settings',
  'type' => MENU_DEFAULT_LOCAL_TASK,
  // Access callback, page callback, and theme callback will be inherited
  // from 'admin/config/foo', if not specified here to override.
);
// Make an additional tab called "Node settings" on "Foo settings"
$items['admin/config/foo/node'] = array(
  'title' => 'Node settings',
  'type' => MENU_LOCAL_TASK,
  // Page callback and theme callback will be inherited from
  // 'admin/config/foo', if not specified here to override.
  // Need to add access callback or access arguments.
);

It says "if not specified here to override" but overriding any of the functional keys does not make sense for the MENU_DEFAULT_LOCAL_TASK, even if the core code explicitly allows overriding the access arguments and inheriting the access callback. This is a conceptual error in core, IMO.

So, please ask the creator of the menu item to fix it.

yched’s picture

Yup, I ended up with something like that in my hook_menu_alter() :

foreach ($items as &$item) {
  if (!isset($item['type']) || $item['type'] != MENU_DEFAULT_LOCAL_TASK) {
    chain_menu_access_chain($item, ...);
  }
  elseif ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
    // Both will be inherited from the parent.
    unset($item['access arguments'], $item['access callback']);
  }
}

Maybe something like the above could be integrated in chain_menu_access_chain() itself when called on a MENU_DEFAULT_LOCAL_TASK, instead of a no-op (D6) or an exception (D7) ? Just remove the access params, and rely on the parent's - that's assuming the parent item will get passed to chain_menu_access_chain() too, but that's only reasonable IMO, since the parent path is the one users will actually visit, so it's very likely to be the one you focus on when altering in access rules.

Specifyng access params (moreover partial ones) on a MENU_DEFAULT_LOCAL_TASK might be a silly idea, but declaring it an error is more like an afterthought from chx, and it's currently definitely and explicitly supported by the menu API, and lots of contrib module out there do it - at least in D6.

Other than that, BTW, the module works great in D6, and IMO is ready for an 1.0 release :-)

salvis’s picture

You mean silently unset the MENU_DEFAULT_LOCAL_TASK's access parameters when chain_menu_access_chain() is called on it, rather than complaining or ignoring? But what do we do if clients do NOT call cmac() for the MENU_DEFAULT_LOCAL_TASKs, as we've been teaching them so far?

It does make sense (and allows us to shift the burden of handling misbehaved third-party modules to the client module), but it's a complete reversal of our strategy. The alternative would be to go out and search for any dependent MENU_DEFAULT_LOCAL_TASK and clean it, but that's a bit of a drag...

@chx: What do you say?

Other than that, BTW, the module works great in D6, and IMO is ready for an 1.0 release :-)

Thanks! Where are you using CMA for D6? That version has seen very little usage so far...

yched’s picture

what do we do if clients do NOT call cmac() for the MENU_DEFAULT_LOCAL_TASKs, as we've been teaching them so far?

If they don't call cmac() on MDLTs then, as currently, then the kind of incorrect access checks on the MDLT, pointed in #35, can happen.
If we go as proposed in #37, the cmac() phpdoc should state something like "if cmac() is applied to a MDLT, it should be applied to the parent item as well, and reversedly".
But we're talking about allowing something that was forbidden so far, not the opposite, so there's no BC break. Less headaches on the calling side ("Am I allowed to call cmac() on this menu item ?"), and removes some buggy cases, all benefit IMO.

Where are you using CMA for D6? That version has seen very little usage so far...

Yeah, not sure why, the module fills a need that's not specific to D7.
Anyway. My use case is a setup serving two separate sites through Domain Access. The sites share some content but are fairly different so most menu callbacks available on one site should be hidden on the other. So I stick an additional menu access callback on top of every menu item, and filter whitelists / blacklists depending on the domain. Gives 403s instead of 404s, but we can live with that.

chx’s picture

Access control on default local tasks are weird, can't imagine a valid use case. We can be broken in that case: garbage in, garbage out. That said, we can be broken in any way we want. If you CMA the parent callback and not the default task, little we can do. So yched's idea is a solid one and I wouldn't worry about #38. I said, little we can do -- but we can check in the access callback whether count($args[0]) == 5 and if not THEN throw the exception.

salvis’s picture

Status: Active » Needs review
StatusFileSize
new2.63 KB

So, how about this (for D6)?

yched’s picture

Status: Needs review » Needs work

Nitpicks :

+++ b/chain_menu_access.moduleundefined
@@ -15,6 +15,14 @@
+ * access but instead clear it, so that sane behavior (i.e. inheriting from

"clear it" is not too clear ;-). "... but instead remove its access properties, so that..."

+++ b/chain_menu_access.moduleundefined
@@ -33,12 +41,14 @@
+ *   TRUE if the chaining succeeded, FALSE if a MENU_DEFAULT_LOCAL_TASK item
+ *   was passed (see the note above). Be sure to chain the parent item in the

I'm not sure the return value is needed at all, but that's probably not really important.

As I see this, the idea is "you can call cmac() indifferently on any menu item. However, be sure apply the same cmac() call to a MENU_DEFAULT_LOCAL_TASK and its parent item".

+++ b/chain_menu_access.moduleundefined
@@ -33,12 +41,14 @@
+    // Inherit the parent's access! See #1186208 for details.

I think the common practice is to provide full http://d.o urls

+++ b/chain_menu_access.moduleundefined
@@ -78,7 +88,13 @@ function _chain_menu_access_callback() {
+    // Something's wrong, probably failed to clear this MENU_DEFAULT_LOCAL_TASK
+    // item (see chain_menu_access_chain()). Deny access!

I think chx proposed an exception here. Might make sense, because silently denying access might cause a fair amount of headscratching ("why is my tab not displaying" ?)

salvis’s picture

Status: Needs work » Needs review
StatusFileSize
new2.65 KB

Thank you for your comments.

"... but instead remove its access properties, so that..."

ok

I'm not sure the return value is needed at all

We don't want to change the API. "TRUE if the chaining succeeded" still holds, and calling cmac() on an MENU_DEFAULT_LOCAL_TASK only doesn't have the (presumably) desired effect, so we cannot claim it succeeded.

I think the common practice is to provide full http://d.o urls

ok

I think chx proposed an exception here.

Yes, we'll do that for D7/D8, but D6 doesn't have exceptions. That's why we didn't have it in D6 before.

Here's the updated patch with the two changes. Can you try this for your usage (that's why I started with D6)?

yched’s picture

Status: Needs review » Reviewed & tested by the community

Right, agreed on your replies.
Patch works for me :-)

salvis’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new3.44 KB
new3.68 KB

I found that I had previously committed a line of code at the top of the wrong function in D6. Here's the fixed version for D6 as well as a version for D7/D8.

salvis’s picture

Status: Needs review » Closed (fixed)

Pushed to the D6/D7/D8 -dev versions.