PHP
Article

Multiple Editors per Node in Drupal 7

By Daniel Sipos

One of the things that makes Drupal great is its flexible user permission system. The out of the box permissions grid we are all familiar with covers most uses cases of controlling what users can and cannot do. It is also very easy for module developers to create new permissions and roles that restrict the logic they implement.

Drupal logo

Nevertheless, I have encountered a practical use case where the default configuration options are not enough. Namely, if you need to have multiple users with access to edit a particular node of a given type but without them necessarily having access to edit others of the same type. In other words, the next great article should be editable by Laura and Glenn but not by their colleagues. However, out of the box, users of a particular role can be masters either of their own content or of all content of a certain type. So this is not immediately possible.

In this article I am going to show you my solution to this problem in the form of a simple custom module called editor_list. Article nodes will have a field where you can select users and only these users (or those who have full access) will be able to edit that particular node. You can find the module already in this git repository and you can install it on your site for a quick start. Do keep in mind that it has a dependency on the Entity Reference module as we will see in a minute.

I will keep the code comments to a minimum to save space but you can find them in the repository if you want. Basic knowledge of Drupal 7 is assumed in the remainder of this tutorial.

Scaffolding

We first need the editor_list.info file for our module to get us going:

name = Editor List
description = Module illustrating a custom solution for having multiple editors on a node.
core = 7.x
dependencies[] = entityreference

Next, we need our editor_list.module file where most of our business logic will be located. So go ahead and create it and we will populate it as we go on.

Finally, though not covered here, we can have an editor_list.install file where we can implement hook_install() and hook_update hooks to create fields and/or deploy configuration. In the repository, you’ll find that I provided an install hook that already creates an entity reference field called field_editors and attaches it to the Article content type. If you are following along but not using the code in the repository, you should go ahead and create the field manually through the UI. It’s a simple field that references User entities and allows for unlimited selections. Nothing major.

Node access

Going back to our .module file, it’s time to implement our access logic. First though, to make things as flexible and reusable as possible, let’s have a simple function that returns an array of node types to which we apply our access logic:

function editor_list_node_types() {
  return array('article');
}

Since we are only targeting articles, this will suffice. But we will use this function in multiple places so in case we need to target other types as well, we just have to update this array.

Next, let’s write another helpful function that returns all the user IDs set in the editors field of a given node. We will also use this in multiple places:

function editor_list_uids_from_list($node) {
  $users = field_get_items('node', $node, 'field_editors');

  $allowed_uids = array();
  if ($users) {
    $allowed_uids = array_map(function($user) {
      return $user['target_id'];
    }, $users);
  }

  return $allowed_uids;
}

I believe the function is quite self explanatory so I won’t go into details here. Instead, we can turn to our hook_node_access() implementation that gets called by Drupal whenever a user tries to do something with a node (view, edit or delete):

/**
 * Implements hook_node_access().
 */
function editor_list_node_access($node, $op, $account) {
  $node_types = editor_list_node_types();

  if ( ! is_object($node) || ! in_array($node->type, $node_types) || $op !== 'update') {
    return NODE_ACCESS_IGNORE;
  }

  $allowed_uids = editor_list_uids_from_list($node);

  if (empty($allowed_uids)) {
    return NODE_ACCESS_IGNORE;
  }

  if (in_array($account->uid, $allowed_uids)) {
    return NODE_ACCESS_ALLOW;
  }
}

So what’s happening here?

First, we use our previously declared helper function to get the list of node types we want to target, and we basically ignore the situation and return if the node type of the currently accessed node is not within our list or if the operation the user is attempting is not of the type “update”. Then we use our other helper function to check if there are any users in the editor list for this node and again ignore the situation if there aren’t. However, if there are, and our accessing user is among them, we return the NODE_ACCESS_ALLOW constant which basically gives the user access to perform the attempted operation. And that’s it.

You can check out the documentation for more information about how this hook works.

Let’s say you have admin users who can create and edit any type of content and regular authenticated users who cannot edit articles (apart from maybe the ones they created themselves). Adding one of these latter users to a node’s editor list would give them access to that particular node. And another great thing is that since this is all nicely integrated, contextual filters and tabs also take these dynamic permissions into account.

Field access

We now have a working module that does what I initially set out for it to do. But let’s say that your admin users are the only ones responsible for adding users to the editor lists. In other words, you are afraid that if your editors can edit their nodes and remove themselves from the list, they’ll get locked out of the node they are supposed to work on.

To account for this situation, we need to implement a field access check and remove the possibility that editors tamper with that field. Implementing hook_field_access should do the trick nicely. And if you are wondering, this hook is similar to hook_node_access() but is responsible for individual fields rather than the entire node (+ a couple of other small differences).

/**
 * Implements hook_field_access().
 */
function editor_list_field_access($op, $field, $entity_type, $entity, $account) {
  $node_types = editor_list_node_types();
  if ($entity_type === 'node' && is_object($entity) && in_array($entity->type, $node_types)) {
    return editor_list_control_field_access($op, $field, $entity_type, $entity, $account);
  }
}

And here we have it. There are a few more parameters because this hook gets called for all entities, not just nodes. But again, we check if the currently accessed node is one of those we defined earlier (and that the entity is in fact a node) and this time delegate to another function to keep things tidier:

function editor_list_control_field_access($op, $field, $entity_type, $entity, $account) {
  if ($op !== 'edit') {
    return;
  }

  $uids = editor_list_uids_from_list($entity);
  if (!in_array($account->uid, $uids)) {
    return;
  }

  $deny = array('field_editors');
  if (in_array($field['field_name'], $deny)) {
    return false;
  }
}}

Since we only care if the user is trying to update a particular field, we return nothing if this is not the case. Keep in mind that the op string here is edit and not update as it was in the other hook. This is just one of those Drupal quirks of inconsistency we all came to love so much. And like before, we ignore the situation if the current user is not part of the editor list.

Then, we define an array of field names we want to deny access to (in our case only one but we can add to it depending on the use case). Finally, we return false if the currently accessed field is part of our $deny array. Yet another difference here in that we have to return a boolean instead of a constant like we did before.

Now the editors in the list of a given node cannot remove themselves or add anybody else to the list. But then again, in some cases you may want this functionality and in others not. It’s up to you.

Tidying up

The last thing I am going to show you here relates to organization and maybe a bit of user experience. With our current implementation, the editor list field on the Article nodes is present somewhere on the form (wherever you dragged-and-dropped it when editing the field settings). However, wouldn’t it be nice if it were automatically part of the Authoring information group at the bottom of the page? Something like this:

Drupal 7 multiple editors per node

I think so. Let’s see how we can do that.

First, we need to implement hook_form_alter or one of its variations. I prefer the most targeted one to avoid unnecessary calls to it and a bunch of conditional checks:

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function editor_list_form_article_node_form_alter(&$form, &$form_state, $form_id) {
  $form['#after_build'][] = 'editor_list_node_form_after_build';
}

We went with the BASE_FORM_ID of the article nodes here so if we extend our application to other types we would do the same for those as well. Inside, we just define an #after_build function to be triggered when the form has finished building. This is to ensure all the form alterations have been already done by contributed modules. All that is left to be done is to write the function responsible for making changes to the form:

function editor_list_node_form_after_build($form, &$form_state) {
  $field = field_info_field('field_editors');
  if ( ! field_access('edit', $field, 'node', $form['#entity'])) {
    return $form;
  }

  if ($form['author']['#access'] === 0) {
    return $form;
  }

  $field_editors = $form['field_editors'];
  $field_editors['#weight'] = 0;
  $form['author']['additional_authors'] = $field_editors;
  $form['field_editors'] = array();

  return $form;
}

This looks complicated but it really isn’t. We begin by loading the field definition of our editor list field. This is so that we can run the field_access check on it and just return the form array unchanged if the current user doesn’t have access to the field. Next, we do the same if the current user does not have access to the author group on the form (this is the Authoring information group we want to place the field into). And lastly, we make a copy of the field definition, change its weight and place it into the group, followed by unsetting the original definition to avoid having duplicates.

And that is pretty much it. Now the editors list field should be tucked in with the rest of the information related to authorship.

Conclusion

In this article, we created a solution to a content editing problem that Drupal 7 could not fix out of the box. However, it did provide us with the development tools necessary to make this an easy task inside of a custom module.

We now have an editor list field on the article node form by which we can specify exactly which users have access to that particular node. Though do keep in mind that in order for this to be of any use, the users you add to these lists must not have a role that allows them to edit all article nodes. Otherwise you won’t see much of a difference.

  • Engin Yilmaz
  • http://webomelette.com Danny

    Hey there! Maybe indeed partly it does. To be honest wasn’t even aware of this module. But in any case, the point is to learn a bit about the internals of how you can tailor everything exactly to your needs if module A or B doesn’t do all that you want. Thanks for showing me the module though.

  • Engin Yilmaz

    Hi, I just thought “there is a module for that” and wanted to share it. :)

  • Traian Muresan

    Cool! I love this!

    In fact I was looking for a module like this. Thanks

  • Derek Ahmedzai

    Thanks, this is really useful. Are you planning to release it as a module on drupal.org?

    • http://webomelette.com Danny

      Hey there,

      Glad you find it useful but it was meant more for demonstrating the use of Drupal APIs to achieve this task. As Engin pointed out, there is this module already that provides some of the functionality (and probably a lot more):

      https://www.drupal.org/project/nodeaccess_userreference

      Cheers!

  • Russell Clemings

    I’m getting the following PHP errors on a lot of pages after enabling this module:

    Notice: Undefined index: #entity in editor_list_node_form_after_build() (line 84 of /path/to/sites/all/modules/editor_list/editor_list.module).
    Notice: Undefined index: author in editor_list_node_form_after_build() (line 95 of /path/to/sites/all/modules/editor_list/editor_list.module).
    Notice: Undefined index: field_editors in editor_list_node_form_after_build() (line 106 of /path/to/sites/all/modules/editor_list/editor_list.module).

    It looks as if the problem arises when there are other forms on the page. I made them go away by adding a few lines to the top of the after_build function

    function editor_list_node_form_after_build($form, &$form_state) {
    + // If this isn’t a node edit form, we do nothing
    + if (!isset($form[‘#node_edit_form’])) {
    + return $form;
    + }
    // If the user doesn’t have access to the editors list field, we do nothing.

    Does that sound right?

    • http://webomelette.com Danny

      Hm..not sure why that would happen off the top of my head. The `editor_list_form_article_node_form_alter` implementation makes sure the after build function gets added only to the article node forms and not any of the other ones…So the after build should not get triggered on other forms.

      What other forms do you have on the page? When does the error get triggered?

      Cheers!

      • Russell Clemings

        OK, I see the problem — user error. I’m using this on three different content types so I changed it to “editor_list_form_alter” rather than writing three different versions of the function. Clearly that was a mistake on my part, but I was trying to reduce the number of modifications I’d have to make to add additional content types. Sorry for the noise.

  • http://www.inventivedesign.co.uk Inventive

    Great tutorial. Any plans for a D8 version?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.