Drupal 7: Redirect Users From Unpublished Content

I was recently asked to implement a feature on a Drupal site where all nodes of a certain type would redirect to a main listing page if that node had been unpublished. The problem in doing this is that if a post is unpublished then Drupal will issue an access denied response quite early on in the boot process. When the menu item is loaded it goes through an access callback which sees that the post is unpublished and issues an access denied before anything else can happen. So in this situation you can't use things like Rules to redirect users as the rule is never triggered.

The solution was to use hook_menu_alter() to change the access callback parameter of the node page. We are essentially replacing the normal access callback of node_access() with our own version.

/**
 * Implements hook_menu_alter().
 */
function mymodule_menu_alter(&$items) {
 $items['node/%node']['access callback'] = 'mymodule_access_callback';
}

Here is our own implementation of the new node access callback. What we are interested in is any nodes of the type 'article' that are unpublished and if the user is anonymous. If this is detected then the request is redirected. If it isn't detected then we simply return the normal result of node_access(). There is no need to change the way in which the node permissions work for this so we keep it as it is.

function mymodule_access_callback($op, $node, $account = NULL) {
  if ($node->type == 'article' && $node->status == 0 && ! user_is_logged_in()) {
    drupal_goto('/articles_list_page');
  }
  return node_access($op, $node, $account);
}

This is only half of the story. We also need to take into account what happens if the page is access via Javscript, or if the page is encountered during a cron run. With the above code, if either of these conditions happen then cron will break and you will receive strange JavaScript errors on autocomplete fields. The following adds some more checks to prevent this.

function mymodule_access_callback($op, $node, $account = NULL) {
  // If this is an unpublished article_page.
  if ($node->type == 'article' && $node->status == 0 && ! user_is_logged_in()) {
    // Make sure cron isn't running.
    if (variable_get('cron_semaphore', FALSE) === FALSE && $_SERVER['SCRIPT_NAME'] !== '/cron.php' && arg(3) !== 'run-cron') {
      // Not a JavaScript callback.
      if (arg(0) != 'js') {
        return drupal_goto('/articles_list_page');
      }
    }
  }
  return node_access($op, $node, $account);
}

The above solution to this shows how important knowing the internal boot processes in Drupal and what hook to use in what situation is. We could have used something like hook_init() to do the same thing here, but we would need to load the node before hand and because it would be triggered on every page request the check would be more complex so that we didn't generate any false redirects. By overriding the node access callback in this way we ensure that we are only ever dealing with the correct nodes.

Comments

Thanks for this useful tip. Just one quick question, as I only have limited experience in using callbacks. Do you mean that mymodule (in 'mymodule_access_callback', line 5 in the first snippet) is to be replaced by the name of my module?. And the same thing with myown (in 'myown_access_callback')? Otherwise I would have thought that both of them should have the same arbitrary name and that my module name only should be used in the hook?
Permalink
You are right, that was a mistake on my part. I've corrected the names of the functions now. Thanks for pointing that out! :)
Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
3 + 16 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.