Allowing Content Editors to View Unpublished Content in Drupal 5

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.

Filed under: 

11 Comments

Wonderful!! I started writing a module like this as my first attempt at writing a Drupal module. I didn't get very far though, as other things started to take priority. You can see this at http://drupal.org/project/publishcontent in all its unfinished (really not started at least as far as d.o is concerned) glory. I do have more code, it's just not in the repositories yet. Could we work something out so you are co-maintainer of this module, I'll stick around for support and your code gets incorporated so there's less d.o clutter? I could upload my (mostly working) copy and you could patch from there? Or maybe deprecate my module in favor of your unreleased one?

There's something of a syncronicity going on here; I was going to finish my module by the end of the week! (Maybe not anymore :)

Cheers! Jake

Thanks for the post. Here's a cheer from the sidelines, hoping that Brad and Jake collaborate on slipstreaming this into the Publish Content module.

Great article. Can I translate it in italian language and post into my drupal site with a link to your ? Tnx.

This is one nice trick, but it does not allow users to view those nodes in listings (eg. in Views or whatever listing you have). So it only makes people access the node if they know the URL (I bet you have some kind of custom overview generation code).

The node access system allows you to provide fine grained access to nodes, although it is considerably more complex (and might not be required at all in your case - it also decreases performance). Just providing it as an addendum for those who find the limitation of this approach inappropriate for their needs: http://api.drupal.org/api/file/developer/examples/node_access_example.module/5/source

I don't think the node_access records/grants system works for unpublished nodes making this a tricky problem when you want a group of reviewers that don't have the "administer nodes" permission. Also, unpublished nodes are filtered out of every node list that drupal generates for just normal viewing for the 'access content" permission.

If I build a custom Views view or even code the SQL myself to display a list of unpublished nodes there is still an access permission issue to work around that can't be handled with the node_access table or hook_access which is only called on the module that implements the node type.

Access control in Drupal is pretty tricky and complicated. You also have to be aware that node_access is not called when doing queries - that is where db_rewrite_sql comes in. So its very possible to build node lists of nodes that aren't actually viewable or editable for the current user.

@Jake - You are free to take and use the code provided in this article for any use you see fit. I posted it because I thought it was useful, but I also don't currently have the time to take on any additional project maintainer roles. I am completely willing to talk with you/help you identify a good approach to achieve your development goals. In addition, We're in the process of creating a similarly granular module for publishing permissions, which I can share when complete. You're free to roll that into a module release on d.o as well, if you like.

@Michel - Yes, feel free to translate and add the post to your site, as long as you give attribution.

@Gabor, dldege - That's a really strong point about this not working for content which is being output in a list, rather than accessed directly. I should have included that information in the original post. Dan is correct when saying that node_access won't kick in for unpublished content, here's the snippet of node_access that checks access grants:

 
  // If the module did not override the access rights, use those set in the
  // node_access table.
  if ($op != 'create' && $node->nid && $node->status) {
    $grants = array();
    foreach (node_access_grants($op) as $realm => $gids) {
      foreach ($gids as $gid) {
        $grants[] = "(gid = $gid AND realm = '$realm')";
      }
    }
 
    $grants_sql = '';
    if (count($grants)) {
      $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
    }
 
    $sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";
    $result = db_query($sql, $node->nid);
    return (db_result($result));
  }
 

As you can see grants will never be checked unless the node is published. Which leaves us with what is pretty much with an unworkable system for unpublished nodes that only appear in lists. Just to kick an idea around, I'd propose using the new hook registry in D7 to allow modules to register additional access control functions which are passed the $op and $node parameters just like node_access. Currently only the module which creates the content type is called with hook_access.

In addition I think breaking some stuff out of the 'administer nodes' permission is a good idea. Currently it's pretty limiting, especially in the case of of the content listing at admin/content/node, which all content editors are locked out of without the administer nodes permission. I filed an issue on this subject a little ways back at http://drupal.org/node/239816, but haven't found the time to move forward with it yet.

i'm banging my head against this in D6. the solution i came up with is since all the nodes are "core" nodes is to change their module owner in the {node_ type} table to my module so i could implement hook_access for them. that allowed me control over creation as well as the control over the rest of the {node_ access} operations.

Hey Brad,

Nice work on the code here. Exactly what I was looking for.

I used the override_node_options module in concert to let those same roles publish/unpublish nodes as well.

I wrote it up here… http://www.workhabit.org/allowing-node-publishing-unpublishing-non-node-admin

…and included a downloadable, modularized tarball of your code (feel free to edit out that link if you consider it spammy).

Thanks again!

This post was a huge help! Had to updated a D5 site for a client. One quick note, I needed to modify the module weight so it would take precedence on node_access() (as stated in the article).

To do this, simply find the module that contains the code in the system table and set the weight to something like -999.

Thanks for the post.

and Michel,
Italian translation of the page available here;

http://tinyurl.com/yfz4tne

I am having this exactly problem with work flow and seeing unpublished nodes. This looks the solution I need. I am a littele confused where the 2 snippets listed above need to added. Is it in a module?

Add a comment