monster_menus-9.0.x-dev/modules/mm_workflow_access/mm_workflow_access.module
modules/mm_workflow_access/mm_workflow_access.module
<?php
/**
* @file
* Provides node access permissions based on workflow states.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\monster_menus\Constants;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowState;
const MM_WORKFLOW_PERMS_DELETE = 'd';
const MM_WORKFLOW_AUTHOR_GID = -1;
const MM_WORKFLOW_EVERYONE = 0;
/**
* Implements hook_hook_info().
*/
function mm_workflow_access_module_implements_alter(&$implementations, $hook) {
if (isset($implementations['mm_workflow_access']) && in_array($hook, ['form_alter', 'form_node_form_alter', 'workflow', 'mm_node_access', 'node_links_alter'])) {
unset($implementations['mm_workflow_access']);
$implementations['mm_workflow_access'] = FALSE;
}
}
/**
* Implements hook_theme().
*/
function mm_workflow_access_theme() {
return [
'mm_workflow_access_form' => [
'render element' => 'form',
],
];
}
function mm_workflow_access_preprocess_mm_workflow_access_form(&$variables) {
$form = &$variables['form'];
$variables['no_read'] = $form['no_read'];
unset($form['no_read']);
$rows = [];
foreach (Element::children($form) as $sid) {
$row = [['data' => $form[$sid]['#title']]];
foreach (Element::children($form[$sid]) as $mode) {
$row[] = ['data' => $form[$sid][$mode], 'class' => ['align-top']];
}
$rows[] = ['data' => $row];
}
$header = [
t('Workflow state'),
t('Who can <strong>read</strong> posts in this state'),
t('Who can <strong>edit/read posts</strong> in this state'),
t('Who can <strong>delete</strong> posts in this state'),
];
$form = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
];
}
/**
* Implements hook_form_alter() for workflow_admin_ui_edit_form.
*/
function mm_workflow_access_form_workflow_state_form_alter(&$form, FormStateInterface $form_state) {
/** @var RouteMatchInterface $route */
$route = \Drupal::service('current_route_match');
/** @var Workflow $wf */
$wf = $route->getParameters()->get('workflow_type');
$states = $wf->getStates();
$form['workflow_access'] = [
'#type' => 'details',
'#title' => t('Access control'),
'#open' => TRUE,
'#tree' => TRUE,
'#theme' => 'mm_workflow_access_form',
];
foreach ($states as $state) {
$sid = $state->id();
$access = $state->getThirdPartySetting('mm_workflow_access', 'mm_workflow_access');
$form['workflow_access'][$sid]['#title'] = Markup::create($state->label());
foreach ([Constants::MM_PERMS_READ, Constants::MM_PERMS_WRITE, MM_WORKFLOW_PERMS_DELETE] as $mode) {
$everyone = FALSE;
$author = FALSE;
$groups = [];
if (isset($access[$mode])) {
$everyone = in_array(0, $access[$mode]);
$author = in_array(MM_WORKFLOW_AUTHOR_GID, $access[$mode]);
foreach (array_diff($access[$mode], [0, MM_WORKFLOW_AUTHOR_GID]) as $gid) {
$groups[$gid] = mm_content_get_name($gid);
}
}
$form['workflow_access'][$sid][$mode]['everyone'] = [
'#type' => 'checkbox',
'#default_value' => $everyone,
'#attributes' => ['class' => ['wfe-everyone']],
'#title' => t('everyone'),
'#weight' => 1,
];
$form['workflow_access'][$sid][$mode]['author'] = [
'#type' => 'checkbox',
'#default_value' => $author,
'#title' => t('the author'),
'#attributes' => ['class' => ['wfe-author']],
'#weight' => 2,
];
$form['workflow_access'][$sid][$mode]['groups'] = [
'#type' => 'mm_grouplist',
'#mm_list_popup_start' => mm_content_groups_mmtid(),
'#mm_list_other_name' => "workflow_access[$sid][$mode][everyone]",
'#default_value' => $groups,
'#weight' => 3,
];
}
}
$form['#attached']['library'][] = 'mm_workflow_access/edit_form';
$form['workflow_access']['no_read'] = [
'#type' => 'textarea',
'#title' => t('Message to users who aren\'t permitted to read the content'),
'#default_value' => $wf->options['mm_workflow_access_no_read'] ?? '',
'#rows' => 4,
];
// Place our block comfortably down the page.
$form['submit']['#weight'] = 10;
$form['permissions']['#weight'] = 11;
$form['#submit'][] = '_mm_workflow_access_form_workflow_state_form_submit';
}
function _mm_workflow_access_form_workflow_state_form_submit($form, FormStateInterface $form_state) {
/** @var RouteMatchInterface $route */
$route = \Drupal::service('current_route_match');
/** @var Workflow $wf */
$wf = $route->getParameters()->get('workflow_type');
$wf->options['mm_workflow_access_no_read'] = $form_state->getValue(['workflow_access', 'no_read']);
$wf->save();
foreach ($form_state->getValue('workflow_access') as $sid => $access) {
// Ignore irrelevant keys.
/** @var WorkflowState $state */
if ($sid == 'no_read' || !($state = WorkflowState::load($sid)) || $state->getWorkflowId() != $wf->id()) {
continue;
}
$groups_w = [];
$everyone = $author = FALSE;
$new_access = [];
foreach ($access as $mode => $perms) {
if ($perms['everyone']) {
$new_access[$mode][] = MM_WORKFLOW_EVERYONE;
if ($mode == Constants::MM_PERMS_WRITE) {
$everyone = TRUE;
}
}
else {
$new_access[$mode] = [];
if ($perms['author']) {
$new_access[$mode][] = MM_WORKFLOW_AUTHOR_GID;
if ($mode == Constants::MM_PERMS_WRITE) {
$author = TRUE;
}
}
$new_access[$mode] = array_merge($new_access[$mode], array_keys($perms['groups']));
if ($mode == Constants::MM_PERMS_WRITE) {
$groups_w = $perms['groups'];
}
}
}
$state->setThirdPartySetting('mm_workflow_access', 'mm_workflow_access', $new_access);
$state->save();
// Find all nodes using workflow fields
foreach (_workflow_info_fields() as $field_info) {
$field_name = $field_info->getName();
// Find all nodes using this field with the state being edited.
$nids = \Drupal::entityQuery($field_info->getTargetEntityTypeId())
->accessCheck(FALSE)
->condition($field_name, $sid, '=')
->execute();
/** @var NodeInterface $node */
foreach (Node::loadMultiple($nids) as $node) {
// Update the node's permissions to match the new state perms.
$node->__set('users_w', NULL);
$node->__set('groups_w', $groups_w);
$node->__set('others_w', $everyone);
$node->__set('mm_others_w_force', TRUE);
if ($author) {
$node->setOwnerId($node->__get('workflow_author'));
}
$node->save();
}
}
}
\Drupal::messenger()->addStatus(t('Workflow access permissions updated.'));
}
/**
* Implements hook_form_alter() for workflow_type_edit_form.
*/
function mm_workflow_access_form_workflow_type_edit_form_alter(&$form, FormStateInterface $form_state) {
/** @var Workflow $wf */
$wf = $form_state->getBuildInfo()['callback_object']->getEntity();
$form['basic']['instructions'] = [
'#type' => 'textarea',
'#title' => t('Help text'),
'#default_value' => $wf->options['mm_workflow_access_instructions'] ?? '',
'#rows' => 4,
'#description' => t('Instructions to the user editing a node, describing what to do with the workflow field'),
];
$form['actions']['submit']['#submit'][] = function ($form, FormStateInterface $form_state) {
/** @var Workflow $wf */
$wf = $form_state->getBuildInfo()['callback_object']->getEntity();
$wf->options['mm_workflow_access_instructions'] = $form_state->getValue('instructions');
$wf->save();
};
}
/**
* Implements hook_config_schema_info_alter().
*/
function mm_workflow_access_config_schema_info_alter(&$definitions) {
if (isset($definitions['workflow.workflow.*']['mapping'])) {
// Store instructions and read-only message in the Workflow config entity.
$definitions['workflow.workflow.*']['mapping']['options']['mapping'] += [
'mm_workflow_access_instructions' => ['label' => 'Help Text', 'type' => 'string'],
'mm_workflow_access_no_read' => ['label' => 'Instructions to users without read access', 'type' => 'string'],
];
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for node entities.
*/
function mm_workflow_access_node_presave(NodeInterface $node) {
if (($state = _mm_workflow_access_get_current_state($node)) && ($wf_data = _mm_workflow_access_node_has_workflow($node)) && ($node->isNew() || !isset($node->__get($wf_data['field'])[0]) || $node->__get($wf_data['field'])[0]->get('value')->getString() != $state->id())) {
$node->__set('users_w', []);
$node->__set('groups_w', []);
$node->__set('others_w', FALSE);
$node->setOwnerId(1);
$access = $state->getThirdPartySetting('mm_workflow_access', 'mm_workflow_access');
if (!empty($access[Constants::MM_PERMS_WRITE])) {
foreach ($access[Constants::MM_PERMS_WRITE] as $gid) {
if ($gid == MM_WORKFLOW_EVERYONE) {
// Setting others_w_force makes the change apply in MM, even though
// this user might not normally have permission
$node->__set('others_w', TRUE);
$node->__set('mm_others_w_force', TRUE);
break;
}
elseif ($gid > 0) {
$groups_w = $node->__get('groups_w');
$groups_w[$gid] = '';
$node->__set('groups_w', $groups_w);
}
elseif (!is_null($node->__get('workflow_author'))) {
$node->setOwnerId($node->__get('workflow_author'));
}
}
}
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for node_form.
*/
function mm_workflow_access_form_node_form_alter(&$form, FormStateInterface $form_state) {
/** @var Node $node */
$node = $form_state->getFormObject()->getEntity();
if ($wf_data = _mm_workflow_access_node_has_workflow($node)) {
$wf = Workflow::load($wf_data['id']);
if (isset($wf->options['mm_workflow_access_instructions']) && isset($form[$wf_data['field']])) {
$form[$wf_data['field']]['help'] = [
'#type' => 'item',
'#input' => FALSE,
'#description' => $wf->options['mm_workflow_access_instructions'],
];
}
$form['settings_perms']['help1'] = [
'#weight' => -2,
'#type' => 'item',
'#input' => FALSE,
'#description' => t('This content\'s permissions are controlled by a workflow.'),
];
$form['settings_perms']['#title'] = t('Who can edit this content');
$uid = $node->__get('workflow_author') ?: \Drupal::currentUser()->id();
$group = &$form[$wf_data['field']];
if ($form['settings_perms']['table']['#perms']['allow_everyone']) {
$group['#type'] = 'details';
$group['#open'] = TRUE;
$group['#title'] = t('Workflow');
$group['_workflow_author-choose'] = [
'#type' => 'textfield',
'#title' => t('Choose the author'),
'#autocomplete_route_name' => 'monster_menus.autocomplete',
'#description' => mm_autocomplete_desc(),
'#size' => 30, '#maxlength' => 40,
];
$group['_workflow_author'] = [
'#type' => 'mm_userlist',
'#description' => t('In addition to appearing in the attribution, the content\'s author can be given special permissions within a workflow.'),
'#title' => t('Original Author'),
'#default_value' => [$uid => mm_ui_uid2name($uid)],
'#mm_list_autocomplete_name' => '_workflow_author-choose',
'#mm_list_min' => 1,
'#mm_list_max' => 1,
'#element_validate' => ['mm_workflow_access_validate_workflow_author'],
];
$form['settings_perms']['owner'] = [
'#type' => 'value',
'#value' => 1,
];
}
else {
$group['_workflow_author'] = [
'#type' => 'value',
'#value' => $uid,
];
}
if (!empty($form['settings_perms']['table']['everyone'][0][0]['node-everyone']['#default_value'])) {
$help2 = t('Everyone can edit this content while it is in the current workflow state.');
}
else {
unset($form['settings_perms']['table']['everyone']);
$help2 = t('These users and groups can edit this content while it is at the current stage of the workflow:');
$form['settings_perms']['table']['#readonly'] = TRUE;
$form['settings_perms']['table']['indiv_tbl'][0]['#mm_owner']['show'] = FALSE;
if (empty($form['settings_perms']['table']['groups_tbl'][0]['#mm_groups']) && empty($form['settings_perms']['table']['indiv_tbl'][0]['#mm_users'])) {
unset($form['settings_perms']['table']);
unset($form['#attached']['js']['settings_perms_summary']);
$help2 = t('Only administrators can edit this content while it is at the current stage of the workflow.');
}
}
if (!empty($help2)) {
$form['settings_perms']['help2'] = [
'#weight' => -1,
'#type' => 'item',
'#input' => FALSE,
'#description' => $help2,
];
}
}
}
function mm_workflow_access_validate_workflow_author(&$element, FormStateInterface $form_state) {
$value = $form_state->getValue('_workflow_author');
$form_state->setValue('_workflow_author', [0 => ['target_id' => array_key_first($value)]]);
}
function mm_workflow_access_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
if (isset($form['fields']['_workflow_author'])) {
// Don't allow the widget or order to be changed.
$form['fields']['_workflow_author']['#attributes']['class'] = ['tabledrag-hide'];
$form['fields']['_workflow_author']['plugin']['type']['#access'] = FALSE;
$form['fields']['_workflow_author']['parent_wrapper']['parent']['#access'] = FALSE;
$form['fields']['_workflow_author']['region']['#access'] = FALSE;
$form['fields']['_workflow_author']['weight']['#access'] = FALSE;
}
}
/**
* Implements hook_mm_delete().
*/
function mm_workflow_access_mm_delete($mmtids) {
// First, filter just the groups.
$groups = [];
foreach (mm_content_get($mmtids, Constants::MM_GET_PARENTS) as $tree) {
if (isset($tree->parents[1]) && $tree->parents[1] == mm_content_groups_mmtid()) {
$groups[] = $tree->mmtid;
}
}
if ($groups) {
foreach (WorkflowState::loadMultiple() as $state) {
$changed = FALSE;
if ($access = $state->getThirdPartySetting('mm_workflow_access', 'mm_workflow_access')) {
foreach ($access as $mode => $gids) {
if (array_intersect($gids, $groups)) {
$access[$mode] = array_diff($gids, $groups);
$changed = TRUE;
}
}
}
if ($changed) {
$state->setThirdPartySetting('mm_workflow_access', 'mm_workflow_access', $access);
$state->save();
}
}
}
}
/**
* Implements hook_user_delete().
*/
function mm_workflow_access_user_delete(UserInterface $account) {
// Find all nodes where the workflow author is being deleted.
$nids = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('_workflow_author', $account->id())
->execute();
foreach (Node::loadMultiple($nids) as $node) {
// Change to the admin user.
$node->set('_workflow_author', [0 => ['target_id' => 1]])->save();
}
}
function mm_workflow_access_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
if ($entity_type->id() == 'node') {
$have_workflow_field = FALSE;
$have_workflow_author_field = isset($fields['_workflow_author']);
/** @var FieldConfig $field_config */
foreach ($fields as $field_config) {
$storage = $field_config->getFieldStorageDefinition();
if ($storage->getType() == 'workflow') {
$have_workflow_field = TRUE;
break;
}
}
if ($have_workflow_field != $have_workflow_author_field) {
if ($have_workflow_field) {
$field_config_storage = \Drupal::getContainer()
->get('entity_type.manager')
->getStorage('field_config');
$existing_field = $field_config_storage->loadByProperties([
'entity_type' => 'node',
'bundle' => $bundle,
'field_name' => '_workflow_author',
]);
if (empty($existing_field)) {
$fields['_workflow_author'] = $field_config_storage->create([
'entity_type' => 'node',
'field_name' => '_workflow_author',
'label' => t('Workflow Author'),
'bundle' => $bundle,
])->save();
}
else {
$fields['_workflow_author'] = $existing_field;
}
}
else {
unset($fields['_workflow_author']);
}
Cache::invalidateTags(['entity_field_info']);
}
}
}
/**
* Implements hook_entity_field_storage_info().
*/
function mm_workflow_access_entity_field_storage_info(EntityTypeInterface $entity_type) {
$fields = [];
if ($entity_type->id() == 'node') {
// Create the field storage if it doesn't already exist.
$field_storage_storage = \Drupal::getContainer()
->get('entity_type.manager')
->getStorage('field_storage_config');
$field_storage = $field_storage_storage->load('node._workflow_author');
if (empty($field_storage)) {
$field_storage = $field_storage_storage->create([
'field_name' => '_workflow_author',
'entity_type' => 'node',
'type' => 'entity_reference',
'locked' => TRUE,
'settings' => [
'target_type' => 'user',
],
]);
$field_storage->save();
}
$fields['_workflow_author'] = $field_storage;
}
return $fields;
}
/**
* Implements hook_ENTITY_TYPE_load().
*/
function mm_workflow_access_node_load(array $nodes) {
/** @var NodeInterface $node */
foreach ($nodes as $node) {
if (_mm_workflow_access_node_has_workflow($node)) {
$node->__set('workflow', workflow_node_current_state($node));
$node->__set('workflow_author', $node->get('_workflow_author')->getValue()[0]['target_id'] ?? NULL);
if (!is_null($node->__get('workflow_author')) && ($account = User::load($node->__get('workflow_author')))) {
$node->__set('workflow_author_name', $account->getAccountName());
}
// @FIXME: set cache tag here
}
}
}
/**
* Implements hook_node_view().
*/
function mm_workflow_access_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
// Prevent the user from seeing content based on their ability to read at
// this stage of the workflow
if (($wf_data = _mm_workflow_access_node_has_workflow($node)) && !_mm_workflow_access_get_user_perm($node, Constants::MM_PERMS_READ)) {
$node->__set('content', [
'body' => ['#markup' => Workflow::load($wf_data['id'])->options['mm_workflow_access_no_read'] ?? ''],
]);
$node->__set('mm_workflow_access_read_denied', TRUE);
}
}
/**
* Implements hook_link_alter().
*/
function mm_workflow_access_node_links_alter(array &$links, NodeInterface $node) {
if (!empty($node->mm_workflow_access_read_denied)) {
unset($links['comment_add']);
}
}
/**
* Implements hook_mm_node_access().
*/
function mm_workflow_access_mm_node_access($op, NodeInterface $node, AccountInterface $account) {
if ($node->id() && _mm_workflow_access_node_has_workflow($node)) {
if ($op == 'view') {
$mode = Constants::MM_PERMS_READ;
}
elseif ($op == 'update') {
$mode = Constants::MM_PERMS_WRITE;
}
elseif ($op == 'delete') {
$mode = MM_WORKFLOW_PERMS_DELETE;
}
else {
return;
}
if (is_null($node->__get('workflow'))) {
$node->__set('workflow', workflow_node_current_state($node));
}
return _mm_workflow_access_get_user_perm($node, $mode, $account);
}
}
function _mm_workflow_access_node_has_workflow(NodeInterface $node) {
static $cache;
// Return TRUE only if the workflow form is to be displayed and a workflow
// is assigned to this node type
$type = $node->getType();
if (!$type) {
return FALSE;
}
if (!isset($cache[$type])) {
$cache[$type] = FALSE;
foreach ($node->getFieldDefinitions() as $definition) {
if ($definition->getType() == 'workflow') {
$cache[$type] = ['id' => $definition->getSettings()['workflow_type'], 'field' => $definition->getName()];
}
}
}
return $cache[$type];
}
function _mm_workflow_access_get_perms(NodeInterface $node, AccountInterface $account = NULL) {
static $cache;
if (!isset($account)) {
$account = \Drupal::currentUser();
}
$wf_data = _mm_workflow_access_node_has_workflow($node);
if (is_null($account->id()) || !$wf_data || is_null($node->__get('workflow_author'))) {
return [];
}
if ($account->hasPermission('administer all menus')) {
return [Constants::MM_PERMS_READ, Constants::MM_PERMS_WRITE, MM_WORKFLOW_PERMS_DELETE];
}
$uid = $account->id();
if (!isset($cache[$uid][$node->__get('workflow')][$node->__get('workflow_author')])) {
$cache[$uid][$node->__get('workflow')][$node->__get('workflow_author')] = [];
// This is somewhat expensive, so only perform if needed, and cache the
// result for the duration of this function call.
$user_can_update = function () use ($node, $account, &$user_can_update_result) {
if (!isset($user_can_update_result)) {
$user_can_update_result = mm_content_user_can_update_node($node, $account);
}
return $user_can_update_result;
};
/** @var WorkflowState $state */
if ($state = WorkflowState::load($node->__get('workflow'))) {
$access = $state->getThirdPartySetting('mm_workflow_access', 'mm_workflow_access');
if ($access) {
foreach ($access as $mode => $gids) {
foreach ($gids as $gid) {
if ($gid == MM_WORKFLOW_EVERYONE ||
$gid == MM_WORKFLOW_AUTHOR_GID && ($node->workflow_author == $uid || $user_can_update()) ||
$gid > 0 && in_array($uid, mm_content_get_uids_in_group($gid))) {
$cache[$uid][$node->__get('workflow')][$node->__get('workflow_author')][] = $mode;
}
}
}
}
}
if ($account->hasPermission('view all menus')) {
$cache[$uid][$node->__get('workflow')][$node->__get('workflow_author')][] = Constants::MM_PERMS_READ;
}
}
return $cache[$uid][$node->__get('workflow')][$node->__get('workflow_author')];
}
function _mm_workflow_access_get_user_perm(NodeInterface $node, $mode, $account = NULL) {
if (empty($account)) {
$account = \Drupal::currentUser();
}
// If this is a new node, give the owner full access to it.
if ($node->isNew() && $node->getOwnerId() == $account->id()) {
return TRUE;
}
$list = _mm_workflow_access_get_perms($node, $account);
// Write also includes read
if ($mode == Constants::MM_PERMS_READ && in_array(Constants::MM_PERMS_WRITE, $list)) {
return TRUE;
}
return in_array($mode, $list);
}
function _mm_workflow_access_get_current_state(NodeInterface $node) {
if (!empty($node->__get('workflow')) && ($loaded = WorkflowState::load($node->__get('workflow')))) {
return $loaded;
}
if ($workflow = workflow_get_workflows_by_type($node->getType(), 'node')) {
return $workflow->getCreationState();
}
}
