I'm using TAC to control edit permissions by taxonomy term for a single content type. The issue I'm trying to solve is that currently any content moderator with the Revisioning module permission "Publish revisions of any [content-type] content" is capable of publishing pending revisions of nodes that they do not have edit permissions on (since this is restricted by TAC). I'd like the "Publish" menu tab to be hidden whenever the user does not have edit permission on the node. I'm looking for advice on the best approach for this. Currently, on line 322 of revisioning_theme.inc I notice this menu link is run through the theme function, and I'm considering overriding this somehow in a custom theme function. Is this a good approach?

Comments

shaundychko’s picture

Status: Active » Closed (fixed)

Rules came to the rescue on this one. The "Publish" link is still shown to moderators who do not have edit permissions on nodes with the particular taxonomy term, but the publish link will redirect to the node view without publishing the revision. The rule tests whether the acting user has edit permission on the node, and it requires that the PHP Filter module is enabled.

array (
  'rules' => 
  array (
    'rules_categorize_revision_publishing' => 
    array (
      '#type' => 'rule',
      '#set' => 'event_revisioning_pre_publish',
      '#label' => 'categorize revision publishing',
      '#active' => 1,
      '#weight' => '0',
      '#categories' => 
      array (
      ),
      '#status' => 'custom',
      '#conditions' => 
      array (
        0 => 
        array (
          '#weight' => 0,
          '#negate' => 1,
          '#type' => 'condition',
          '#settings' => 
          array (
            'boolean' => '<?php $response = node_access("update", $current_revision, $user); return $response; ?>',
            '#eval input' => 
            array (
              'token_rules_input_evaluator' => 
              array (
                'boolean' => 
                array (
                  0 => ':global',
                ),
              ),
              'rules_input_evaluator_php' => 
              array (
                'boolean' => 
                array (
                  0 => 'current_revision',
                  1 => 'user',
                ),
              ),
            ),
          ),
          '#info' => 
          array (
            'label' => 'acting user can update node',
            'label callback' => false,
            'arguments' => 
            array (
              'boolean' => 
              array (
                'type' => 'boolean',
                'label' => 'Truth value',
              ),
            ),
            'module' => 'Rules',
          ),
          '#name' => 'rules_condition_check_boolean',
        ),
      ),
      '#actions' => 
      array (
        0 => 
        array (
          '#type' => 'action',
          '#settings' => 
          array (
            'message' => 'You do not have permission to publish nodes in this category.',
            'error' => 1,
            '#eval input' => 
            array (
              'token_rules_input_evaluator' => 
              array (
                'message' => 
                array (
                  0 => ':global',
                ),
              ),
            ),
          ),
          '#name' => 'rules_action_drupal_message',
          '#info' => 
          array (
            'label' => 'Show a configurable message on the site',
            'module' => 'System',
            'eval input' => 
            array (
              0 => 'message',
            ),
          ),
          '#weight' => 0,
        ),
        1 => 
        array (
          '#weight' => 0,
          '#type' => 'action',
          '#settings' => 
          array (
            'path' => '[current_revision:node-url]',
            'query' => '',
            'fragment' => '',
            'force' => 1,
            'immediate' => 1,
            '#eval input' => 
            array (
              'token_rules_input_evaluator' => 
              array (
                'path' => 
                array (
                  0 => 'current_revision',
                  1 => ':global',
                ),
                'query' => 
                array (
                  0 => ':global',
                ),
                'fragment' => 
                array (
                  0 => ':global',
                ),
              ),
            ),
          ),
          '#name' => 'rules_action_drupal_goto',
          '#info' => 
          array (
            'label' => 'Page redirect',
            'module' => 'System',
            'eval input' => 
            array (
              0 => 'path',
              1 => 'query',
              2 => 'fragment',
            ),
          ),
        ),
      ),
      '#version' => 6003,
    ),
  ),
)
rdeboer’s picture

Status: Closed (fixed) » Active

Hi Shaun,
Without delving into it too deep, I believe this might work.
Another approach may also work, if in your use-case you are able to align (groups of) taxonomy terms with content types and roles. Because in 6.x-3.13 publication permissions are optionally per content type, you could have, say, a Sports publisher who can only publish content of type Sport, which potentially terms of, say, Basketball, Football, Swimming, etc...

BTW did you try this tutorial: http://drupal.org/node/408018 ? Did that give you any inspiration?
Rik

shaundychko’s picture

Thank you Rik for such a quick reply. I'd like to keep the number of content types to a minimum and use taxonomy to segment the moderators by term since this will give greater flexibility down the road. The client, if they wish, can just add another taxonomy term, associate it with a new moderator role, and apply it to a group of nodes. Yes, I tried that tutorial, and it was great. The one limitation however is that it suggests removing view permissions on nodes the moderator doesn't publish. This is not ideal since I'd have some explaining to do when parts of the site disappear for moderators logging in who believe they have "special permissions". The tutorial acknowledges this and suggests using http://drupal.org/project/taxonomy_access (TAC) instead of TACLite. This is what I've done, but the new issue that pops up is that moderators who do not have edit permission on a node (as a result of TAC limiting their edit permission), can nevertheless publish a pending revision since this permission is, as you say, on a content type basis.

Do you think that in most use cases a node publisher will also have edit permission on that node? If so, perhaps a node_access("update", $node, $user) belongs in the module somewhere before enabling the "Publish" revision menu tab?

One reason I went with Rules, by the way, is a limitation on the theme override approach: if a moderator for one taxonomy group (Basketball) really wants to publish a pending revision from a different term (Baseball), they can manually construct the publish URL .../node/[nid]/revisions/[vid]/publish even if the Publish link isn't displayed. Perhaps this could also be taken into account? I know this is getting picky, but this is a node access module afterall...

Thank you so much for the great tutorials you have for this module (and for the module itself!).

rdeboer’s picture

Title: assign publish permission by category » Assign publish permission by category
Version: 6.x-3.9 » 6.x-3.13

Thanks Shaun for reporting back on all the experimentation you've done. Some great insights and solutions for all to enjoy, so I might refer to this thread from the Revisioning project page.

Quoted from #3:
Do you think that in most use cases a node publisher will also have edit permission on that node? If so, perhaps a node_access("update", $node, $user) belongs in the module somewhere before enabling the "Publish" revision menu tab?

Yes I'd say this is often the case in real-life scenario's. I'd say that those that are allowed to publish a node, e.g. the moderators, are usually allowed to make some minor modifications, before publishing that content. And yes, maybe the code should enforce this. With core's {node_access} lacking a "grant_publish" column, Revisioning should perhaps tie the publish grant in with "grant_update" (one could indeed argue that a publication is a form of "update").
This would then also solve the issue of the moderator maliciously constructing by hand the URL to publish content of a term he does not have access to, right?

Food for thought...

Rik

shaundychko’s picture

Yep, you understand this perfectly Rik, and I agree with what you're saying. I'd also point out that Revisioning already ties the edit permission together with the publish permission as it relates to the "pending revisions" block. This block will alert the moderator about pending revisions only if they also have edit permissions to the node (which is a relief since I'd have a much harder time solving that one). So, since this logic (ie publishers also must have edit permission) is already part of one aspect of the Revisioning module, is seems reasonable to apply that to the "Publish" menu tab as well.

rdeboer’s picture

Assigned: Unassigned » rdeboer

I made it a configurable option on the master branch (i.e 7.x).
Will backport to 6.x branch (i.e 3.14) soon.

shaundychko’s picture

Status: Needs review » Active

Hello Rik, I got inspired to learn more about the Drupal API and came up with the following solution for my use case. This module provides a permission on the regular user permissions page to publish content for each term ID. I wanted to make sure publishers also have node edit permission, and segment the publisher permission by taxonomy term instead of by content type.

Known limitations:
1) I didn't want to monkey too much with the revisioning module (thanks for such great commenting in your code, by the way!) so instead of overriding the menu access callback of revisioning's publish link, this module creates it's own publish link. If a user has both permission to publish the content type from the revisioning permission, and permission to publish content with the term ID, they will see two publish links.

2) I used term ID reluctantly since it isn't user friendly. Nevertheless I went this way since tid is guaranteed to be unique.

3) When a term is deleted, I suppose this module should delete all permissions to publish with that ID. I don't think this is a real issue since it's guaranteed that all future terms will have a different tid since the tid autoincrements, but having obsolete publish permissions left over after term deletion isn't very clean.

This module also adds a comment in the revision log stating which username published the node, and when.
The vocabularies that are enabled for providing access to the publish link is set using a checkbox on the vocabulary edit form.

Do you think this module is worth adding to Revisioning, or whether others might find it useful as a separate module? If so, I don't mind cleaning up any loose ends.


//Vocabularies designated to control publishing permission are set on the
//vocabulary edit form.

/**
 * Implementation of hook_menu().
 */
function rtp_menu() {
  $items = array();
  
  $items['node/%node/revisions/%/rtp'] = array(
    'load arguments' => array(3),
    'title' => 'Publish this revision',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_publish_confirm', 1),
    'access callback' => 'rtp_publish_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => -7,
    'tab_parent' => 'node/%/revisions',
  );
  return $items;
}

/**
 * menu publish callback
 */
function rtp_publish_access($node) {
  if (!isset($node->num_revisions) || !isset($node->is_current)) {
    drupal_set_message(t('Node object data incomplete -- have you enabled the Node Tools submodule?'), 'warning', FALSE);
  }
  if (!rtp_publish_appropriate($node)) {
    return FALSE;
  }
  //initialize $access variable to false
  $access = FALSE;
  
  $type = check_plain($node->type);
  $rtp_vids = variable_get('rtp_vids', NULL);
  $terms = $node -> taxonomy;
  foreach($terms as $the_term) {
    //check whether the vocabulary of the term is used for rtp publishing
    if(in_array($the_term -> vid, $rtp_vids)) {
      //build permission
      $perm = 'Publish content with term ID ' .
          $the_term -> tid;
      global $user;
      if((user_access($perm) && module_grants_node_access("update", $node, $user)) ||
          user_access('administer nodes')) {
        $access = TRUE;
      }
    }
  }
  return $access;
}

/**
 * check if it's appropriate to publish this revision.
 */
function rtp_publish_appropriate($node) {
  // If the node isn't meant to be moderated and the user is not an admin,
  // or the revision is not either pending or current but not published,
  // then disallow publication.
  if ((!$node->revision_moderation && !user_access('administer nodes'))
    || !($node->is_pending || ($node->is_current && !$node->status))) {
    return FALSE;
  } else {
    return TRUE;
  }
}

/**
 * Implementation of hook_perm()
 * Some vocabularies control publish permission. Create a permission for each
 * term in these vocabularies. These vocabularies are selected from the
 * Vocabulary edit form.
 */
function rtp_perm() {
  
  $perms = array();
  $vocabularies = taxonomy_get_vocabularies();
  foreach($vocabularies as $the_vocabulary) {
    //check if the vocabulary is one that controls publish permission
    $vid = $the_vocabulary -> vid;
    $rtp_vids = variable_get('rtp_vids', NULL);
    if(in_array($vid, $rtp_vids)) {
      $terms = taxonomy_get_tree($vid);
      
      foreach($terms as $the_term) {
        $perms[] = 'Publish content with term ID ' .
          $the_term -> tid;
      }
    }
  }
  return $perms;
  
}

/**
 * Implementation of hook_revisionapi()
 * append Published by [username] to the revision log, with the date.
 */
function rtp_revisionapi($op, &$node) {
  global $user;
  if($op == 'post publish') {
    $log_message = $node -> log . ' Published by ' . $user -> name . ' on ' .
      format_date(time(), 'medium');
    db_query("UPDATE {node_revisions} SET log='%s' WHERE vid=%d",
      $log_message, $node->vid);
  }
}

/**
 * Implementation of hook_form_FORM_ID_alter()
 *
 * Put a checkbox on the vocabulary form to designate it's terms for controlling revisioning publish
 * permission
 */
function rtp_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
  //load vocabulary id's already set to control publish permission
  $rtp_vids = variable_get('rtp_vids', NULL);
  $current_vid = $form['vid']['#value'];
  
  //create checkbox on vocabulary edit form
  $form['settings']['rtp'] = array(
    '#type' => 'checkbox',
    '#title' => t('Control permission to publish revisions'),
    '#description' => t('Use terms in this vocabulary to control permission to publish revisions. Setting this will cause
                        term ID\'s in this vocabulary to appear on the user permissions form.'),
    '#default_value' => in_array($current_vid, $rtp_vids),
    '#weight' => -1,
  );
  
  //add rtp submit function to array of vocabulary submit processing functions
  $form['#submit'][] = 'rtp_vocabulary_submit';
  
}

/**
 * Submit function to populate rtp_vids array with vocabularies
 * when vocabulary form is submitted.
 */
function rtp_vocabulary_submit(&$form, &$form_state) {
  
  $rtp_vids = variable_get('rtp_vids', NULL);
  $current_vid = $form['vid']['#value'];
  
  switch ($form_state['values']['rtp']) {
    case TRUE:
      //if the vocabulary ID is not already in the lists of vocabularies that
      //control publishing permission, then add it.
      if(!in_array($current_vid, $rtp_vids)) {
        $rtp_vids[] = $current_vid;
        variable_set('rtp_vids', $rtp_vids);
      }
      break;
    
    case FALSE:
      //if the vocab is in the rtp_vids array, then remove it
      $the_key = array_search($current_vid, $rtp_vids);
      //note "Not identical" operator since the array key could be zero.
      if($the_key !== FALSE) {
        unset($rtp_vids[$the_key]);
        variable_set('rtp_vids', $rtp_vids);
      }
      break;
  }
}
shaundychko’s picture

Status: Active » Needs review
shaundychko’s picture

Status: Active » Needs review

Just for fun I created a sandbox project here: http://drupal.org/sandbox/ShaunDychko/1260258

rdeboer’s picture

Status: Needs review » Closed (fixed)

Well it's all at http://drupal.org/sandbox/ShaunDychko/1260258
Closing this issue.