/**
* Implementation of hook_menu_alter().
*/
function joke_menu_alter(&$callbacks) {
// If the user does not have 'administer nodes' permission,
// disable the joke menu item by setting its access callback to FALSE.
if (!user_access('administer nodes')) {
$callbacks['node/add/joke']['access callback'] = FALSE;
// Must unset access arguments or Drupal will use user_access()
// as a default access callback.
unset($callbacks['node/add/joke']['access arguments']);
}
}
The above function is from the pro development drupal. I can't understand it well. Why must I unset the access arguments (unset($callbacks['node/add/joke']['access arguments']);)?
Thank you.
That entire example seems broken and bad. In short, a joke. First, let me answer your question, then I'll go on to explain why you shouldn't follow that example in practice.
From includes/menu.inc:
if (!isset($item['access callback']) && isset($item['access arguments'])) {
// Default callback.
$item['access callback'] = 'user_access';
}
Unsetting the access callbacks when you no longer need them (relying on a boolean now, after all) prevents the over-clever logic in Drupal's routing system from slapping in user_access() just so it has something to do.
Now, on to why that's bad code.
hook_menu() and hook_menu_alter() are both run on cache clear (more specifically when the menu routing system is rebuilt). This means that the permissions of whichever user hits the site to rebuild the menus will be hard-coded into menu routing behaviors. This is a very bad and inconsistent arrangement.
If you want to block access to a path based on a permission, you need to change the callback to something that will test for that permission. Then when the menu is rebuilt, it will check the new callback function per page load to see if the current user should be granted permission.
A simple example of this might look like:
/**
* Implementation of hook_menu_alter().
*/
function joke_menu_alter(&$items) {
$items['node/add/joke']['access callback'] = 'user_access';
$items['node/add/joke']['access arguments'] = array('administer nodes');
}
Now we have a function which takes the node/add/joke path and declares that the only thing that matters is whether or not the user has administer nodes permission. Of course, that's a little more limited than the apparent intentions of the example, which were to preserve the existing access controls, but also require the user to have administer nodes permission.
That is also fixable, but is more complicated. To borrow some concepts from the Spaces project:
/**
* Implementation of hook_menu_alter().
*/
function joke_menu_alter(&$items) {
$path = 'node/add/joke';
$items[$path]['access arguments'][] = $items[$path]['access callback'];
$items[$path]['access callback'] = 'joke_menu_access';
}
function joke_menu_access() {
$args = func_get_args();
$access_callback = array_pop($args);
$original_access = call_user_func_array($access_callback, $args);
return $original_access && user_access('administer nodes');
}
We have successfully wrapped the original access callback in a new access callback, to which we can add whatever additional logic we need.
Note that in the last two function examples, I used the $path variable to keep the code simple. I also separated $original_access to it's own line and had it checked first, in practice I would check user_access() first as it would almost certainly be more performant than whatever happens in the original access callback.
The comment directly above that line explains it?
access callback is the function that is called (or TRUE/FALSE) and arguments is what is passed to that function. You are setting the callback to false and therefore always deny access to that router item.
And now, as the comment is saying, you also needto unset the arguments or Drupal will still use user_access() (The default access callback).
Related
I was wondering if someone would be kind enough to provide a meteorpad or code example of using one of the methods listed above properly in Meteor (with iron:router). I'm struggling to understand how exactly these methods interact with my app, and it seems these methods are new enough that there isn't much good documentation on how to use them correctly. Thanks!
http://docs.meteor.com/#/full/Accounts-onResetPasswordLink
Ok, so I am going to post what I ended up learning and doing here so others can use it as a reference. I'll do my best to explain what is happening as well.
As can be seen in the other comments, the 'done' function passed to the Accounts.on****Link callback was the main part that tripped me up. This function only does one thing - re-enables autoLogin. It's worth noting that the 'done' function/autoLogin is a part of one of the core 'accounts' packages, and cannot be modified. 'autoLogin' is used in one particular situation: User A tries to reset his or her pw on a computer where User B is currently logged in. If User A exits the reset password flow before submitting a new password, then User B will remain logged in. If User A completes the reset password flow, then User B is logged out and User A is logged in.
The pattern used to handle 'done' in the accounts-ui package, and what I ended up doing, assigns 'done' to a variable that can then be passed to your template event handler function, and run once your reset password logic is complete. This variable assignment needs to be done in the Accounts.on****Link callback, but the callback can be placed in any top-level client side code (just make sure you assign the scope of the variables correctly). I just put it at the start of my reset_password_template.js file (I've only done this for resetting passwords so far, but the pattern should be similar):
client/reset_password_template.js:
// set done as a variable to pass
var doneCallback;
Accounts.onResetPasswordLink(function(token, done) {
Session.set('resetPasswordToken', token); // pull token and place in a session variable, so it can be accessed later
doneCallback = done; // Assigning to variable
});
The other challenge of using these on****Link callbacks is understanding how your app 'knows' the callback has been fired, and what needs to be done by the app. Since iron:router is so tightly integrated with Meteor, it's easy to forget it is a separate package. It's important to keep in mind these callbacks were written to operate independently of iron:router. This means when the link sent to your email is clicked, your app is loaded at the root level ('/').
***Side note - There are some other answers here on StackOverflow that offer ways to integrate with iron:router, and load a specific route for each link. The problem for me with these patterns was that they seemed a bit hackish, and not in line with the 'meteor' way. More importantly, if the core Meteor team decides to alter the path of these registration links, these routes would break. I tried calling Router.go('path'); in the on****Link callback, but for some reason this didn't work in Chrome and Safari. I would love to have a way to handle specific routes for each of these emailed links, thus eliminating the need for constantly setting and clearing Session variables, but I couldn't think of a good solution that worked.
Anyways, as #stubailo described in his answer, your app is loaded (at the root level), and the callback is fired. Once the callback is fired, you have your session variable set. You can use this session variable to load the appropriate templates at the root level using the following pattern:
client/home.html (or your landing page template)
{{#unless resetPasswordToken}}
{{> home_template}}
{{else}}
{{> reset_password_template}}
{{/unless}}
With this, there are few things you need to take care of in your reset_password_template.js file, and home.js:
client/home.js
// checks if the 'resetPasswordToken' session variable is set and returns helper to home template
Template.home.helpers({
resetPasswordToken: function() {
return Session.get('resetPasswordToken');
}
});
client/reset_password_template.js
// if you have links in your template that navigate to other parts of your app, you need to reset your session variable before navigating away, you also need to call the doneCallback to re-enable autoLogin
Template.reset_password_template.rendered = function() {
var sessionReset = function() {
Session.set('resetPasswordToken', '');
if (doneCallback) {
doneCallback();
}
}
$("#link-1").click(function() {
sessionReset();
});
$('#link2').click(function() {
sessionReset();
});
}
Template.reset_password_template.events({
'submit #reset-password-form': function(e) {
e.preventDefault();
var new_password = $(e.target).find('#new-password').val(), confirm_password = $(e.target).find('#confirm-password').val();
// Validate passwords
if (isNotEmpty(new_password) && areValidPasswords(new_password, confirm_password)) {
Accounts.resetPassword(Session.get('resetPasswordToken'), new_password, function(error) {
if (error) {
if (error.message === 'Token expired [403]') {
Session.set('alert', 'Sorry, this link has expired.');
} else {
Session.set('alert', 'Sorry, there was a problem resetting your password.');
}
} else {
Session.set('alert', 'Your password has been changed.'); // This doesn't show. Display on next page
Session.set('resetPasswordToken', '');
// Call done before navigating away from here
if (doneCallback) {
doneCallback();
}
Router.go('web-app');
}
});
}
return false;
}
});
Hopefully this is helpful for others who are trying to build their own custom auth forms. The packages mentioned in the other answers are great for many cases, but sometimes you need additional customization that isn't available via a package.
I wrote this method, so hopefully I can give a good example of how to use it.
It's meant to be in conjunction with Accounts.sendResetPasswordEmail and Accounts.resetPassword (http://docs.meteor.com/#/full/accounts_sendresetpasswordemail and http://docs.meteor.com/#/full/accounts_resetpassword).
Basically, let's say you want to implement your own accounts UI system instead of using the accounts-ui package or similar. If you want to have a password reset system, you need three things:
A way to send an email with a password reset link
A way to know when the user has clicked the reset link
A method to actually reset the password
Here is how the flow should work:
The user clicks a link on your page that says "Reset password"
You find out which user that is (possibly by having them enter their email address), and call Accounts.sendResetPasswordEmail
The user clicks the reset password link in the email they just received
Your app is loaded and registers a callback with Accounts.onResetPasswordLink
The callback is called because the URL has a special fragment in it with the password reset token
This callback can display a special UI element that asks the user to input their new password
The app calls Accounts.resetPassword with the token and the new password
Now the user is logged in and they have a new password
This is a little complicated because it is the most advanced and custom flow possible. If you don't want to mess around with all of these callbacks and methods, I would recommend using one of the existing accounts UI packages, for example accounts-ui or https://atmospherejs.com/ian/accounts-ui-bootstrap-3
For some example code, take a look at the code for the accounts-ui package: https://github.com/meteor/meteor/blob/devel/packages/accounts-ui-unstyled/login_buttons_dialogs.js
Per the documentation:
You can construct your own user interface using the functions below, or use the accounts-ui package to include a turn-key user interface for password-based sign-in.
Therefore, those callback are for rolling your own custom solution. However, I would recommend using one of the following packages below, with accounts-entry being my preferred solution:
Use a combination of accounts-password and accounts-ui
Or use https://atmospherejs.com/joshowens/accounts-entry, especially if you want OAuth integrations such as Facebook, Twitter, etc. For handling email verification with this package, please see this Github issue.
It's been a year since this question but I just came up with the same problem.
Following your solution, what I found is that you could use the Session variable within the router and the onAfterAction hook to achieve the same, but using routes:
Router.route('/', {
name: 'homepage',
action: function() {
if (Session.get('resetPasswordToken')) {
this.redirect('resetPassword', {token: Session.get('resetPasswordToken')});
} else {
this.render('home');
}
}
});
Router.route('/password/reset/:token', {
name: 'resetPassword',
action: function () {
this.render('resetPassword');
},
data: function() {
return {token: this.params.token};
},
onAfterAction: function () {
Session.set('resetPasswordToken', '');
}
});
Of course, you will need also:
Accounts.onResetPasswordLink(function(token, done){
Session.set('resetPasswordToken', token);
doneResetPassword = done;
});
I would like to limit the use of some url's. Let's say node/add and node/7 (just random examples). I'm thinking the best way to do this is to use the user_access function.
But as we are used to it, the Drupal documentation doesn't help much. When I just use the function, I get the message the function is already in use. So my best guess is to use this existing function with my own arguments in my custom function in my custom module.
But in this way I need to catch the page before loading it. Or I'm I missing something here?
EDIT:
I've set this
global $user;
$items['node/add/%']['access callback'] = array('_mymodule_node_access');
$items['node/add/%']['access arguments'] = array(0,2, $user);
But for some reason, Drupal isn't picking up the % card for all types. It's just working for one type (script). Other terms like page or fiche aren't getting picked up... % is a Drupal wildcard right?
EDIT:
I just found out there are already some paths in the database. How can I overwrite them? What I need is one selector which can select all four content types (fiche, page, script and news-item).
The way to define a particular access function for a path is to set the access callback for the path's menu item in hook_menu(). This is slightly different for existing paths, in that you need to implement hook_menu_alter() to edit the existing access callback for that path:
function mymodule_menu_alter(&$items) {
$items['node/add']['access callback'] = 'mymodule_node_add_access_callback';
}
function mymodule_node_add_access_callback() {
// return TRUE to allow access, FALSE to deny
}
This gets a bit more fun when we're talking about node pages as their menu items is defined using a wildcard node/%. This means that using hook_menu_alter() you can only change the access callback for all nodes.
Fortunately Drupal has a hook_node_access hook to come to the rescue:
function mymodule_node_access($node, $op, $account) {
$restricted_nids = array(7, 10, 12);
if (in_array($node->nid, $restricted_nids) && $op == 'view') {
if ($some_condition_is_true) {
return NODE_ACCESS_ALLOW;
}
return NODE_ACCESS_DENY;
}
return NODE_ACCESS_IGNORE;
}
Hope that helps
EDIT
If that all seems like a bit much hassle you might get some joy installing the Path Access module, I think it has the functionality you're after.
ANOTHER EDIT
I think the reason overriding the wildcard isn't working in this case is because the node module explicitly defines a path for each node type, e.g. node/add/page, node/add/article, etc. Because Drupal will take an exact match (node/add/page) over a wildcard match (node/add/%) you're actually overriding the wrong menu item.
Try specifying the path explicitly in your hook_menu_alter() function (note that the access callback should be a string and not an array as you currently have):
$items['node/add/page']['access callback'] = '_mymodule_node_access';
$items['node/add/page']['access arguments'] = array(0,2, $user);
It's also worth noting that the $user object you're passing will always be the user object of the logged in user who cleared Drupal's caches (since menu items are rebuilt when the cache is rebuilt). If you're looking to pass the current logged in user (i.e. the one logged in at the time the page is accessed) that's a different thing altogether...I'd advise asking another question on it as it can be a tricky bugger and you want to get as much input as possible from people on here.
This snippet of code responds for user#1 but not for other logged in or anonymous users:
function module_menu_alter(&$items) {
$items["node/add/page"]['access callback'] = 'module_access_callback';
}
function module_access_callback(){
die('responding here - test');
}
What am I doing wrong?
If that is the code you are really using, then the access callback function is wrong, as it should return TRUE when the currently logged-in user has access to the menu, and FALSE when the user doesn't have access to the menu. It doesn't use die().
This is what reported in the documentation for hook_menu():
"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'.
If you are not showing the code you are using, then what follows can explain the behavior you are seeing.
The first user, or user #1, is a particular user because user_access() always return TRUE for that user.
This is evident in the code of that function, which explicitly check the user ID is equal to 1.
// User #1 has all privileges:
if ($account->uid == 1) {
return TRUE;
}
If the access callback returns FALSE for authenthicated users, and it is using user_access(), it is because the authenticated users don't have the permission passed to the function.
It could also be you are checking for more than one permission, and you are using user_access('first permission') && user_access('second permission'), instead of user_access('first permission') || user_access('second permission') (or vice versa). For the user #1 that doesn't make any difference, as the result of user_access('first permission') && user_access('second permission') and user_access('first permission') || user_access('second permission') is always TRUE, even if you pass to the function a string for a permission that is not defined from any module.
OK It turns out the answer is actually very simple...
You're calling menu_get_object() to retrieve the node, but no node exists on node/add/page. In fact quite the opposite; it wouldn't make sense to have a node available on the page to add a node, as it hasn't been created yet! The 403 is a side effect of this.
You'll need to make your access decision based on some other value (normally the logged in user along with a permission as #kiamlaluno has done a very good job of explaining in his answer).
Also do make sure you return TRUE or FALSE from your access callback as #kiamlaluno has also stated :)
EDIT
Just to say that this is partially an answer to https://stackoverflow.com/questions/8342169/drupal-hook-menu-alter-menu-get-object-error, which explains why there are functions mentioned in this answer that aren't mentioned in the question.
I am seeing a weird bug where occasionally after logging in as user "foo" the username in a logged in block shows "bar" i.e. the $user object has somehow switched from foo to bar.
I've checked all custom modules and the theme to make sure there is no voodoo going on when I use global $user; but no leads.
Also placed additional watchdog messages inside user_authenticate and user_authenticate_finalize but the user object seems to be behaving.
How else can I track this error down?
A user switch can happen with very simple code like this :
global $user;
$some_user = user_load('uid' => SOME_UID);
$user = $some_user;
Perhaps you can try looking at assignments like the last one or uses of global $user
How can one hook exit in a custom module be called in every page request of a drupal website?
Here is the hook exit code in a custom module:
<?php
function cec_monitoring_exit() {
if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') {
// database inserts here
}
}
?>
Is it because of the if condition or someting else? Because some of the custom modules are calling the hook 'cec_monitoring_exit()' but some other custom modules don't.
Thanks in advance.
Cheers,
Mark
I'm not sure what you mean by it is not called in some of the custom modules either, so just speculating here that you might mean custom pages provided by other modules:
As Jeremy and Googletorp stated already, your if block
if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') {
// database inserts here
}
will only evaluate to true if the user is requesting a full node page (e.g. 'node/42'). It will not match any other page like a term view (e.g. 'taxonomy/term/36') or the default frontpage ('node') or a view page provided by the views module (might/have/any/path), etc..
So your database inserts would only take place for node pages, and nothing else.
If you want to log every page request, you can just remove the if block and do your database insertions directly.
Edit after clarification in comments (cec_monitoring_exit() does not get invoked for some pages created by other modules):
I can only see two possible reasons for this.
The first reason would be an error occurring right after the page output, but before the invocation of your hook_exit() implementation (check your server logs for php errors on requests for the failing pages). If you take a look at 'index.php' (top level folder of your Drupal instance):
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$return = menu_execute_active_handler();
// Menu status constants are integers; page content is a string.
if (is_int($return)) {
switch ($return) {
case MENU_NOT_FOUND:
drupal_not_found();
break;
case MENU_ACCESS_DENIED:
drupal_access_denied();
break;
case MENU_SITE_OFFLINE:
drupal_site_offline();
break;
}
}
elseif (isset($return)) {
// Print any value (including an empty string) except NULL or undefined:
print theme('page', $return);
}
drupal_page_footer();
you can see that the page output is generated by printing the result of theme('page', $return). The invocation of hook_exit() happens right after that in drupal_page_footer().
So you should check (preferably via debugger, but you could also use print statements) if drupal_page_footer() gets executed on the pages in question at all. If it gets called, the error might occur in a hook_exit() implementation of another module that gets called before yours, so you'd need to check those.
The second reason would be if one of the modules would circumvent the standard Drupal execution flow by calling theme('page', ...) itself and stopped the execution afterwards. In this case, drupal_page_footer() would not be called, because the execution would have stopped long before during the call to menu_execute_active_handler(). Note that no established Drupal Module would do this, so it is pretty unlikely.
Besides these options, I have no further idea on what could cause this.
Hooks are called whenever their condition is met. This is done with the use of module_invoke_all(). This means that hooks that are called on every page request, will be invoked in every page request. In your example above, your hook wont do anything in some cases, but it will still be called since Drupal wont know when it actually will do something.
This waste of resource will be limited when pages are cached, since you only will need to run the hooks when page cached page is created. Another example is whenever a node is loaded with node_load, this will result in a lot of hooks being fired, and is thus quite expensive. Thus you usually want to avoid using this whenever possible, when you want to access something on a lot of nodes like their title.
#marknt15 what i think your failing to realize here is its all based on name so that hook being the hook_exit() in your case AKA _exit() is called when it is placed with a prefix of the calling modules name eg cec_monitoring_exit() will only work in the cec_monitoring module but in you other custom module named say marknt15_loves_drupal then it would meed to use hook_exit() like marknt15_loves_drupal_exit()
drupal then calls it and all other hooks with ...
call_user_func_array('modulename'_'hookname') or something very similar