You are here

workflow_access.module in Workflow 8

Provides node access permissions based on workflow states.

File

modules/workflow_access/workflow_access.module
View source
<?php

/**
 * @file
 * Provides node access permissions based on workflow states.
 */
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\Role;
use Drupal\workflow\Entity\WorkflowTransition;

/**
 * Implements hook_help().
 */
function workflow_access_help($route_name, RouteMatchInterface $route_match) {
  $output = '';
  switch ($route_name) {
    case 'entity.workflow_type.access_form':
      $url = Url::fromRoute('workflow.access.settings');
      $output .= t('This page lets you refine the permissions per role and per
         workflow state. Although the workflow module allows you to add multiple
         workflows to per entity type, Workflow Access supports only one
         workflow per entity type.');
      $output .= "<br>";
      $output .= t("WARNING: Use of the 'Edit any', 'Edit own', and even 'View\n         published content' permissions for the content type may override these\n         access settings. You may disable those permissions or\n         <a href=':url'>alter the priority of\n        the Workflow access module</a>.", [
        ':url' => $url
          ->toString(),
      ]);
      if (\Drupal::moduleHandler()
        ->moduleExists('og')) {
        $output .= '<br>';
        $output .= t('WARNING: Organic Groups (OG) is present and may interfere
          with these settings.');

        // $output .= ' ';
        // $url = Url::fromUri('admin/config/group/settings'); // @todo D8: FIXME when OG module is ported.
        // $output .= t("In particular, if <a href=':url'>Strict node access
        //  permissions</a> is enabled, since this may override Workflow access
        //  settings.", [':url' => $url]);
        $output .= t('In particular, if <i>Strict node access permissions</i> is enabled,
           since this may override Workflow access settings.');
      }
      break;
    default:
      break;
  }
  return $output;
}

/**
 * @inheritdoc
 */
function workflow_access_workflow_operations($op, EntityInterface $entity = NULL) {

  // @todo Create action link for AccessRoleForm on WorkflowListBuilder.
  $operations = [];
  return $operations;
}

/**
 * Implements hook_ENTITY_TYPE_insert().
 *
 * We use the Role weight as an id.
 * In contrary to content_access module, that uses a 'content_access_roles_gids'
 * config setting.
 *
 * @todo Determine the best way for D8. @see content_access.module.
 * The problem is that node_access table uses Int, whereas the Role Id is string.
 */
function workflow_access_user_role_insert(EntityInterface $entity) {

  // Attend user to Rebuild data, because the weight of a role
  // is the key for workflow_Access.

  /** @var \Drupal\user\RoleInterface $entity */
  node_access_needs_rebuild(TRUE);
}

/**
 * Implements hook_access_ENTITY_TYPE_update().
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 */
function workflow_access_user_role_update(EntityInterface $entity) {

  // Attend user to Rebuild data, because the weight of a role
  // is the key for workflow_Access.

  /** @var \Drupal\user\RoleInterface $entity */
  if ($entity
    ->getWeight() != $entity->original
    ->getWeight()) {

    // Role's weight has changed.
    node_access_needs_rebuild(TRUE);
  }
}

/**
 * Implements hook_node_grants().
 *
 * Supply the workflow access grants. We are simply using
 * roles as access lists, so rids translate directly to gids.
 */
function workflow_access_node_grants(AccountInterface $account, $op) {
  $gids = [];
  $roles = $account
    ->getRoles();
  foreach ($roles as $role) {

    // @todo D8: compare with content_access module.
    $gids[] = workflow_access_get_role_gid($role);
  }
  return [
    'workflow_access' => $gids,
    'workflow_access_owner' => [
      $account
        ->id(),
    ],
  ];
}

/**
 * Helper providing numeric id for role.
 * Copied from content_access.module.
 */
function workflow_access_get_role_gid($rid) {

  // @todo D8: compare with content_access module.
  //  $config = \Drupal::configFactory()->getEditable('content_access.settings');
  //  $roles_gids = $config->get('content_access_roles_gids');
  //  return $roles_gids[$role];
  //
  // Return a weight, avoiding negative values by starting with 100.
  // For 'Author', no role exists.

  /** @var \Drupal\user\RoleInterface $role */
  $role = Role::load($rid);
  $weight = $role ? 100 + $role
    ->getWeight() : 100 - 20;
  return $weight;
}

/**
 * Implements hook_node_access_records().
 *
 * Returns a list of grant records for the passed in node object.
 * This hook is invoked by function node_access_acquire_grants().
 */
function workflow_access_node_access_records(NodeInterface $node) {
  $grants = [];

  // Only relevant for content with Workflow.
  if (!($workflow_field_names = workflow_get_workflow_field_names($node, $node
    ->getEntityTypeId()))) {
    return $grants;
  }

  // Create grants for each translation of the node.
  $priority = workflow_access_get_setting('workflow_access_priority');
  foreach ($node
    ->getTranslationLanguages() as $langcode => $language) {
    $translation = $node
      ->getTranslation($langcode);

    // @todo How to handle not published entities?
    // if (!$translation->isPublished()) {
    //   return;
    // }
    // Get 'author' of this entity. Some entities (e.g., taxonomy_term)
    // do not have a uid. But then again: node_access is only for nodes...
    $uid = $translation
      ->getOwnerId() ? (int) $translation
      ->getOwnerId() : 0;
    $workflow_transitions = [];
    if (isset($translation->workflow_transitions)) {
      $workflow_transitions = $translation->workflow_transitions;
    }
    else {

      // Sometimes, a node is saved without going through workflow_transition_form.
      // E.g.,
      // - when saving a node with workflow_node programmatically with node_save();
      // - when changing a state on a node view page/history tab;
      // - when rebuilding permissions via batch for Workflow Fields.
      // In that case, we need to create the workflow_transitions ourselves to
      // calculate the grants.
      foreach ($workflow_field_names as $field_name) {

        // Create a dummy transition, just to set $workflow_transitions.
        $old_sid = $new_sid = $translation->{$field_name}->value;
        if ($old_sid) {

          /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
          $transition = WorkflowTransition::create([
            $old_sid,
            'field_name' => $field_name,
          ]);
          $transition
            ->setTargetEntity($translation);
          $transition
            ->setValues($new_sid, $translation
            ->getOwnerId(), \Drupal::time()
            ->getRequestTime(), '');
          $workflow_transitions[$field_name] = $transition;
        }
      }
    }
    $count_workflow_fields = 0;
    foreach ($workflow_transitions as $field_name => $transition) {

      // @todo Add support for multiple workflows per entity.
      if (++$count_workflow_fields > 1) {
        continue;
      }
      if (!($current_sid = workflow_node_current_state($translation, $field_name))) {
        continue;
      }
      foreach (workflow_access_get_workflow_access_by_sid($current_sid) as $rid => $grant) {

        // Anonymous ($uid == 0) author is not allowed for role 'Author' (== -1).
        // Both logically (Anonymous having more rights then authenticated)
        // and technically ($gid must be a positive integer).
        if ($uid == 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) {
          continue;
        }
        $grants[] = [
          'realm' => $uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID ? 'workflow_access_owner' : 'workflow_access',
          'gid' => $uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID ? $uid : workflow_access_get_role_gid($rid),
          'grant_view' => (int) $grant['grant_view'],
          'grant_update' => (int) $grant['grant_update'],
          'grant_delete' => (int) $grant['grant_delete'],
          'priority' => $priority,
          'langcode' => $langcode,
          'field_name' => $field_name,
        ];
      }
    }
  }
  return $grants;
}

/**
 * Implements hook_node_access_explain().
 *
 * This is a Devel Node Access hook.
 */
function workflow_access_node_access_explain($row) {
  static $interpretations = [];
  switch ($row->realm) {
    case 'workflow_access_owner':
      $interpretations[$row->gid] = t('Workflow access: author of the content may access');
      break;
    case 'workflow_access':
      $roles = user_roles();
      $interpretations[$row->gid] = t('Workflow access: %role may access', [
        '%role' => $roles[$row->gid],
      ]);
      break;
  }
  return !empty($interpretations[$row->gid]) ? $interpretations[$row->gid] : NULL;
}

/**
 * DB functions - all DB interactions are isolated here to make for easy updating should our schema change.
 */

/**
 * Given a sid, retrieve the access information and return the row(s).
 */
function workflow_access_get_workflow_access_by_sid($sid) {
  $result = \Drupal::config('workflow_access.role')
    ->get($sid);
  if ($result == NULL) {

    // Avoid errors in calling function when no data available.
    $result = [];
  }
  return $result;
}

/**
 * Given a sid, delete all access data for this state.
 */
function workflow_access_delete_workflow_access_by_sid($sid) {
  \Drupal::configFactory()
    ->getEditable('workflow_access.role')
    ->clear($sid)
    ->save();
}

/**
 * Given data, insert into workflow access - we never update.
 */
function workflow_access_insert_workflow_access_by_sid($sid, &$data, Config $config = NULL) {
  $config = \Drupal::configFactory()
    ->getEditable('workflow_access.role');
  $config
    ->set($sid, $data)
    ->save();
}

/**
 * Get the module settings.
 *
 * @see WorkflowAccessSettingsForm::submitForm()
 */
function workflow_access_get_setting($value) {
  $config = \Drupal::config('workflow_access.settings');
  $priority = $config
    ->get($value);
  return $priority;
}

Functions

Namesort descending Description
workflow_access_delete_workflow_access_by_sid Given a sid, delete all access data for this state.
workflow_access_get_role_gid Helper providing numeric id for role. Copied from content_access.module.
workflow_access_get_setting Get the module settings.
workflow_access_get_workflow_access_by_sid Given a sid, retrieve the access information and return the row(s).
workflow_access_help Implements hook_help().
workflow_access_insert_workflow_access_by_sid Given data, insert into workflow access - we never update.
workflow_access_node_access_explain Implements hook_node_access_explain().
workflow_access_node_access_records Implements hook_node_access_records().
workflow_access_node_grants Implements hook_node_grants().
workflow_access_user_role_insert Implements hook_ENTITY_TYPE_insert().
workflow_access_user_role_update Implements hook_access_ENTITY_TYPE_update().
workflow_access_workflow_operations @inheritdoc