You are here

lightning_workflow.module in Lightning Workflow 8.2

Same filename and directory in other branches
  1. 8.3 lightning_workflow.module
  2. 8 lightning_workflow.module

Provides workflow enhancements for Drupal.

File

lightning_workflow.module
View source
<?php

/**
 * @file
 * Provides workflow enhancements for Drupal.
 */
use Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\lightning_core\Element;
use Drupal\lightning_core\Routing\RouteSubscriber;
use Drupal\lightning_workflow\Plugin\views\field\NodeBulkForm;
use Drupal\node\NodeTypeInterface;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
use Drupal\workflows\Entity\Workflow;

/**
 * Implements hook_ENTITY_TYPE_insert().
 */
function lightning_workflow_node_type_insert(NodeTypeInterface $node_type) {

  // Don't do anything during a config sync.
  if (\Drupal::isConfigSyncing()) {
    return;
  }
  $workflow = $node_type
    ->getThirdPartySetting('lightning_workflow', 'workflow');
  if (empty($workflow)) {
    return;
  }
  $workflow = Workflow::load($workflow);
  if (empty($workflow)) {
    return;
  }
  $plugin = $workflow
    ->getTypePlugin();
  if ($plugin instanceof ContentModerationInterface) {
    $rebuild = !in_array('node', $plugin
      ->getEntityTypes(), TRUE);
    $plugin
      ->addEntityTypeAndBundle('node', $node_type
      ->id());
    $workflow
      ->save();

    // We need to rebuild all routes because Content Moderation needs to ensure
    // that edit forms load the latest revision.
    if ($rebuild) {
      Drupal::service('router.builder')
        ->rebuild();
    }
  }
}

/**
 * Implements hook_theme_registry_alter().
 */
function lightning_workflow_theme_registry_alter(array &$theme_registry) {
  foreach ($theme_registry as $hook => &$info) {
    if ($hook == 'field' || isset($info['base hook']) && $info['base hook'] == 'field') {

      // We wrap around Quick Edit's preprocess function, so it should not be
      // run directly.
      $info['preprocess functions'] = array_diff($info['preprocess functions'], [
        'quickedit_preprocess_field',
      ]);
    }
  }
}

/**
 * Implements template_preprocess_field().
 */
function lightning_workflow_preprocess_field(array &$variables) {
  if (\Drupal::moduleHandler()
    ->moduleExists('quickedit')) {
    quickedit_preprocess_field($variables);

    /** @var \Drupal\Core\Entity\EntityInterface $entity */
    $entity = $variables['element']['#object'];
    if ($entity instanceof EntityPublishedInterface && $entity
      ->isPublished() && RouteSubscriber::isViewing($entity)) {
      unset($variables['attributes']['data-quickedit-field-id']);
    }
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function lightning_workflow_module_implements_alter(array &$implementations, $hook) {

  // We have to check for hook_node_view_alter() because of absolute insanity
  // in ModuleHandler::alter() and the way it determines the implementations of
  // 'secondary' alter hooks. It's weird logic that is pretty close to
  // inexplicable...but trust me, to wrap around quickedit_entity_view_alter(),
  // we need to alter the implementations of hook_node_view_alter(). Granted,
  // this will only work for nodes. If we want to do this for another entity
  // type, we'll have to check for its entity type-specific view_alter hook as
  // well.
  if ($hook == 'node_view_alter') {
    unset($implementations['quickedit']);
  }
}

/**
 * Implements hook_entity_view_alter().
 */
function lightning_workflow_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
  if (\Drupal::moduleHandler()
    ->moduleExists('quickedit')) {
    quickedit_entity_view_alter($build, $entity, $display);
    if ($entity instanceof EntityPublishedInterface && $entity
      ->isPublished() && RouteSubscriber::isViewing($entity)) {
      unset($build['#attributes']['data-quickedit-entity-id']);
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 *
 * TODO: Re-implement a relationship to the latest revision, and add a field
 * indicating if forward revisions exist. This is a known regression due to
 * incomplete functionality in Content Moderation.
 */
function lightning_workflow_view_presave(ViewEntityInterface $view) {

  // Don't do anything during config sync.
  if (\Drupal::isConfigSyncing()) {
    return;
  }
  elseif ($view
    ->id() == 'content' && $view
    ->isNew()) {
    $display =& $view
      ->getDisplay('default');
    $display_options =& $display['display_options'];

    // Allow users to filter by moderation state.
    $display_options['filters']['moderation_state'] = unserialize('a:15:{s:2:"id";s:16:"moderation_state";s:5:"table";s:15:"node_field_data";s:5:"field";s:16:"moderation_state";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:8:"operator";s:2:"in";s:5:"value";a:5:{s:3:"all";s:3:"all";s:15:"editorial-draft";s:15:"editorial-draft";s:16:"editorial-review";s:16:"editorial-review";s:19:"editorial-published";s:19:"editorial-published";s:18:"editorial-archived";s:18:"editorial-archived";}s:5:"group";i:1;s:7:"exposed";b:1;s:6:"expose";a:11:{s:11:"operator_id";s:19:"moderation_state_op";s:5:"label";s:16:"Moderation state";s:11:"description";s:0:"";s:12:"use_operator";b:0;s:8:"operator";s:19:"moderation_state_op";s:10:"identifier";s:16:"moderation_state";s:8:"required";b:0;s:8:"remember";b:0;s:8:"multiple";b:0;s:14:"remember_roles";a:10:{s:13:"authenticated";s:13:"authenticated";s:9:"anonymous";s:1:"0";s:13:"administrator";s:1:"0";s:12:"page_creator";s:1:"0";s:14:"layout_manager";s:1:"0";s:13:"page_reviewer";s:1:"0";s:20:"landing_page_creator";s:1:"0";s:21:"landing_page_reviewer";s:1:"0";s:13:"media_creator";s:1:"0";s:13:"media_manager";s:1:"0";}s:6:"reduce";b:0;}s:10:"is_grouped";b:0;s:10:"group_info";a:10:{s:5:"label";s:0:"";s:11:"description";s:0:"";s:10:"identifier";s:0:"";s:8:"optional";b:1;s:6:"widget";s:6:"select";s:8:"multiple";b:0;s:8:"remember";b:0;s:13:"default_group";s:3:"All";s:22:"default_group_multiple";a:0:{}s:11:"group_items";a:0:{}}s:11:"entity_type";s:4:"node";s:9:"plugin_id";s:23:"moderation_state_filter";}');

    // Ditch status field and the "published"/"published or admin" filters.
    unset($display_options['filters']['status'], $display_options['filters']['status_extra'], $display_options['fields']['status']);

    // Add a field to display the moderation state.
    $display_options['fields']['moderation_state'] = unserialize('a:37:{s:2:"id";s:16:"moderation_state";s:5:"table";s:35:"content_moderation_state_field_data";s:5:"field";s:16:"moderation_state";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:16:"Moderation state";s:5:"label";s:16:"Moderation state";s:7:"exclude";b:0;s:5:"alter";a:26:{s:10:"alter_text";b:0;s:4:"text";s:0:"";s:9:"make_link";b:0;s:4:"path";s:0:"";s:8:"absolute";b:0;s:8:"external";b:0;s:14:"replace_spaces";b:0;s:9:"path_case";s:4:"none";s:15:"trim_whitespace";b:0;s:3:"alt";s:0:"";s:3:"rel";s:0:"";s:10:"link_class";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:6:"target";s:0:"";s:5:"nl2br";b:0;s:10:"max_length";i:0;s:13:"word_boundary";b:1;s:8:"ellipsis";b:1;s:9:"more_link";b:0;s:14:"more_link_text";s:0:"";s:14:"more_link_path";s:0:"";s:10:"strip_tags";b:0;s:4:"trim";b:0;s:13:"preserve_tags";s:0:"";s:4:"html";b:0;}s:12:"element_type";s:0:"";s:13:"element_class";s:0:"";s:18:"element_label_type";s:0:"";s:19:"element_label_class";s:0:"";s:19:"element_label_colon";b:1;s:20:"element_wrapper_type";s:0:"";s:21:"element_wrapper_class";s:0:"";s:23:"element_default_classes";b:1;s:5:"empty";s:3:"N/A";s:10:"hide_empty";b:0;s:10:"empty_zero";b:0;s:16:"hide_alter_empty";b:1;s:17:"click_sort_column";s:5:"value";s:4:"type";s:6:"string";s:8:"settings";a:1:{s:14:"link_to_entity";b:0;}s:12:"group_column";s:5:"value";s:13:"group_columns";a:0:{}s:10:"group_rows";b:1;s:11:"delta_limit";i:0;s:12:"delta_offset";i:0;s:14:"delta_reversed";b:0;s:16:"delta_first_last";b:0;s:10:"multi_type";s:9:"separator";s:9:"separator";s:2:", ";s:17:"field_api_classes";b:0;s:11:"entity_type";s:24:"content_moderation_state";s:12:"entity_field";s:16:"moderation_state";s:9:"plugin_id";s:5:"field";}');
    Element::toTail($display_options['fields'], 'operations');
  }
}

/**
 * Implements hook_modules_installed().
 */
function lightning_workflow_modules_installed(array $modules) {

  // Don't do anything during config sync.
  if (\Drupal::isConfigSyncing()) {
    return;
  }
  if (in_array('lightning_roles', $modules, TRUE)) {
    \Drupal::service('lightning.content_roles')
      ->grantPermissions('creator', [
      'use editorial transition create_new_draft',
      'use editorial transition review',
      'view any unpublished content',
      'view latest version',
      'use moderation sidebar',
    ])
      ->grantPermissions('reviewer', [
      'use editorial transition publish',
      'use editorial transition review',
      'use editorial transition archive',
      'view any unpublished content',
      'view latest version',
      'access toolbar',
      'use moderation sidebar',
    ]);
  }
  if (in_array('lightning_dev', $modules, TRUE)) {

    /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */
    $module_installer = Drupal::service('module_installer');

    // Install Lightning Page separately in order to ensure that the optional
    // Pathauto config that it ships is installed too.
    $module_installer
      ->install([
      'lightning_page',
    ]);

    // Lightning Workflow optionally integrates with Diff, and for testing
    // purposes we'd like to enable that integration. In order to test with
    // meaningful responsibility-based roles, we also enable Lightning Roles.
    $module_installer
      ->install([
      'diff',
      'lightning_roles',
      'moderation_sidebar',
      'pathauto',
      'quickedit',
      'toolbar',
      'views',
    ]);

    // BigPipe breaks a couple of non-JS tests which expect to find local task
    // links, but receive BigPipe placeholders instead. This is a bit of a
    // sledgehammer approach, but until it's possible to disable a module during
    // a single test, it's the best we can do.
    $module_installer
      ->uninstall([
      'big_pipe',
    ]);

    // Add moderation to the page content type.

    /** @var NodeTypeInterface $node_type */
    $node_type = entity_load('node_type', 'page');
    $node_type
      ->setThirdPartySetting('lightning_workflow', 'workflow', 'editorial');
    lightning_workflow_node_type_insert($node_type);

    // Allow the content view to filter by moderation state.
    $view = entity_load('view', 'content')
      ->enforceIsNew();
    lightning_workflow_view_presave($view);
    $view
      ->enforceIsNew(FALSE)
      ->save();
  }
}

/**
 * Implements hook_views_data_alter().
 */
function lightning_workflow_views_data_alter(array &$data) {
  foreach ($data as $table => $table_data) {
    if (isset($table_data['moderation_state']['field'])) {
      $data[$table]['moderation_state']['field'] += [
        'id' => 'null',
      ];
    }
  }
}

/**
 * Implements hook_views_plugins_field_alter().
 */
function lightning_workflow_views_plugins_field_alter(array &$plugins) {
  if (isset($plugins['node_bulk_form'])) {
    $plugins['node_bulk_form']['class'] = NodeBulkForm::class;
  }
}

/**
 * Implements hook_entity_field_access().
 *
 * Tests that rely on promoting a node to the front page cannot access the
 * "Promote to front page" checkbox without the "administer nodes" permission.
 * That totally sucks, so this hook will allow access to the field if
 * lightning_dev is enabled.
 */
function lightning_workflow_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  return Drupal::moduleHandler()
    ->moduleExists('lightning_dev') && $field_definition
    ->getTargetEntityTypeId() === 'node' && $field_definition
    ->getName() === 'promote' && $items ? $items
    ->getEntity()
    ->access($operation === 'edit' ? 'update' : $operation, $account, TRUE) : AccessResult::neutral();
}

/**
 * Implements hook_views_pre_render().
 */
function lightning_workflow_views_pre_render(ViewExecutable $view) {
  if ($view
    ->id() == 'moderation_history') {
    foreach ($view->result as $index => $row) {
      $entity = $row->_entity;
      if (empty($previous) || $previous->moderation_state->value != $entity->moderation_state->value) {
        $previous = $entity;
      }
      else {
        unset($view->result[$index]);
        $view->total_rows--;
      }
    }
  }
}