The Problem
A couple weeks ago we received a call from a client who was having problems giving content editors access to nodes which were in the unpublished state. The particular client was recently upgraded from a Drupal 4.7 to Drupal 5.7. In addition to upgrading the site, we also redesigned and integrated a second Drupal site into the same installation. One of the goals for this redesign was to be able to easily provision content editors for each site, allowing their internal teams full control over the content that appeared on their site, and their site alone. The problem we ran into when trying to do this was that the "administer nodes" permission was too much of a catch-all for us to be able to effectively designate permissions how we needed. Particularly of concern was the inability for content editors to access unpublished content, since many of the nodes on the site are used for internal purposes and never intended for general public consumption. After the break we'll have our solution in all it's nerdy glory.
The Cause
The node.module implementation of hook_menu maps the path node/nid to the node_page_view() function with this menu item:
$items[] = array( 'path' => 'node/'. arg(1), 'title' => t('View'), 'callback' => 'node_page_view', 'callback arguments' => array($node), 'access' => node_access('view', $node), 'type' => MENU_CALLBACK, );
What we want to avoid here is call to node_access, which will always return false when a given user is viewing unpublished content which they didn't create, while not having the 'administer nodes' permission.
The Solution
The first step is to build permissions for viewing unpublished content of each type in the system:
/*
-
Build a permissions list for viewing unpublished nodes of all content types.
-
Also, provide a 'use view_unpublished module' permission which determines if this module
-
will even attempt to override the default node/nid path. */ function view_unpublished_perm() { $perms = array('use view_unpublished module', 'view all unpublished content'); $types = db_query("select type from {node_type}"); $i = 0; while ($type = db_result($types, $i++)) { $perms[] = 'view unpublished '. $type .' content'; }
return $perms; }
Once we have our nice view unpublished content checkboxes for each content type, we can do the heavy lifting:
/*
- Selectively overrides the node/nid path to set access => true when a user has permission
- to view unpublished content */ function view_unpublished_menu($may_cache) { $items = array(); if (!$may_cache) { if (is_numeric(arg(1)) && arg(0) == 'node' && user_access('use view_unpublished module') && !user_access('administer nodes')) { $node = node_load(arg(1)); if ($node->status == 0 && (user_access('view unpublished '. $node->type .' content') || user_access('view all unpublished content'))) { $items[] = array( 'path' => 'node/'. arg(1), 'title' => t('View'), 'callback' => 'node_page_view', 'callback arguments' => array($node), 'type' => MENU_CALLBACK, 'access' => true, ); } } }
return $items; }
the view_unpublished_menu() function simply checks to see if there's an incoming request for a path in the form of node/nid. In the event that it does, the node is unpublished, and a role you belong to has permissions to view unpublished nodes of that content type, the menu path gets overridden with access set to true. This completely circumvents the call to node_access() which was giving us problems. It should be noted that you may have to adjust the module's weight to make sure it's implementation of hook_menu is called after the one in node.module, so you can correctly override the menu path set in node.module.