1<?php
2
3namespace Drupal\content_moderation\Plugin\WorkflowType;
4
5use Drupal\content_moderation\ModerationInformationInterface;
6use Drupal\Core\Entity\ContentEntityInterface;
7use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
8use Drupal\Core\Entity\EntityTypeManagerInterface;
9use Drupal\Core\Entity\EntityPublishedInterface;
10use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
11use Drupal\Core\StringTranslation\StringTranslationTrait;
12use Drupal\content_moderation\ContentModerationState;
13use Drupal\workflows\Plugin\WorkflowTypeBase;
14use Drupal\workflows\StateInterface;
15use Drupal\workflows\WorkflowInterface;
16use Symfony\Component\DependencyInjection\ContainerInterface;
17
18/**
19 * Attaches workflows to content entity types and their bundles.
20 *
21 * @WorkflowType(
22 *   id = "content_moderation",
23 *   label = @Translation("Content moderation"),
24 *   required_states = {
25 *     "draft",
26 *     "published",
27 *   },
28 *   forms = {
29 *     "configure" = "\Drupal\content_moderation\Form\ContentModerationConfigureForm",
30 *     "state" = "\Drupal\content_moderation\Form\ContentModerationStateForm"
31 *   },
32 * )
33 */
34class ContentModeration extends WorkflowTypeBase implements ContentModerationInterface, ContainerFactoryPluginInterface {
35
36  use StringTranslationTrait;
37
38  /**
39   * The entity type manager.
40   *
41   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
42   */
43  protected $entityTypeManager;
44
45  /**
46   * The entity type bundle info service.
47   *
48   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
49   */
50  protected $entityTypeBundleInfo;
51
52  /**
53   * The moderation information service.
54   *
55   * @var \Drupal\content_moderation\ModerationInformationInterface
56   */
57  protected $moderationInfo;
58
59  /**
60   * Constructs a ContentModeration object.
61   *
62   * @param array $configuration
63   *   A configuration array containing information about the plugin instance.
64   * @param string $plugin_id
65   *   The plugin_id for the plugin instance.
66   * @param mixed $plugin_definition
67   *   The plugin implementation definition.
68   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
69   *   The entity type manager.
70   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
71   *   The entity type bundle info.
72   * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
73   *   Moderation information service.
74   */
75  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ModerationInformationInterface $moderation_info) {
76    parent::__construct($configuration, $plugin_id, $plugin_definition);
77    $this->entityTypeManager = $entity_type_manager;
78    $this->entityTypeBundleInfo = $entity_type_bundle_info;
79    $this->moderationInfo = $moderation_info;
80  }
81
82  /**
83   * {@inheritdoc}
84   */
85  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
86    return new static(
87      $configuration,
88      $plugin_id,
89      $plugin_definition,
90      $container->get('entity_type.manager'),
91      $container->get('entity_type.bundle.info'),
92      $container->get('content_moderation.moderation_information')
93    );
94  }
95
96  /**
97   * {@inheritdoc}
98   */
99  public function getState($state_id) {
100    $state = parent::getState($state_id);
101    if (isset($this->configuration['states'][$state->id()]['published']) && isset($this->configuration['states'][$state->id()]['default_revision'])) {
102      $state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']);
103    }
104    else {
105      $state = new ContentModerationState($state);
106    }
107    return $state;
108  }
109
110  /**
111   * {@inheritdoc}
112   */
113  public function workflowHasData(WorkflowInterface $workflow) {
114    return (bool) $this->entityTypeManager
115      ->getStorage('content_moderation_state')
116      ->getQuery()
117      ->condition('workflow', $workflow->id())
118      ->count()
119      ->accessCheck(FALSE)
120      ->range(0, 1)
121      ->execute();
122  }
123
124  /**
125   * {@inheritdoc}
126   */
127  public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
128    return (bool) $this->entityTypeManager
129      ->getStorage('content_moderation_state')
130      ->getQuery()
131      ->condition('workflow', $workflow->id())
132      ->condition('moderation_state', $state->id())
133      ->count()
134      ->accessCheck(FALSE)
135      ->range(0, 1)
136      ->execute();
137  }
138
139  /**
140   * {@inheritdoc}
141   */
142  public function getEntityTypes() {
143    return array_keys($this->configuration['entity_types']);
144  }
145
146  /**
147   * {@inheritdoc}
148   */
149  public function getBundlesForEntityType($entity_type_id) {
150    return isset($this->configuration['entity_types'][$entity_type_id]) ? $this->configuration['entity_types'][$entity_type_id] : [];
151  }
152
153  /**
154   * {@inheritdoc}
155   */
156  public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) {
157    return in_array($bundle_id, $this->getBundlesForEntityType($entity_type_id), TRUE);
158  }
159
160  /**
161   * {@inheritdoc}
162   */
163  public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) {
164    if (!isset($this->configuration['entity_types'][$entity_type_id])) {
165      return;
166    }
167    $key = array_search($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE);
168    if ($key !== FALSE) {
169      unset($this->configuration['entity_types'][$entity_type_id][$key]);
170      if (empty($this->configuration['entity_types'][$entity_type_id])) {
171        unset($this->configuration['entity_types'][$entity_type_id]);
172      }
173      else {
174        $this->configuration['entity_types'][$entity_type_id] = array_values($this->configuration['entity_types'][$entity_type_id]);
175      }
176    }
177  }
178
179  /**
180   * {@inheritdoc}
181   */
182  public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
183    if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
184      $this->configuration['entity_types'][$entity_type_id][] = $bundle_id;
185      sort($this->configuration['entity_types'][$entity_type_id]);
186      ksort($this->configuration['entity_types']);
187    }
188  }
189
190  /**
191   * {@inheritdoc}
192   */
193  public function defaultConfiguration() {
194    return [
195      'states' => [
196        'draft' => [
197          'label' => 'Draft',
198          'published' => FALSE,
199          'default_revision' => FALSE,
200          'weight' => 0,
201        ],
202        'published' => [
203          'label' => 'Published',
204          'published' => TRUE,
205          'default_revision' => TRUE,
206          'weight' => 1,
207        ],
208      ],
209      'transitions' => [
210        'create_new_draft' => [
211          'label' => 'Create New Draft',
212          'to' => 'draft',
213          'weight' => 0,
214          'from' => [
215            'draft',
216            'published',
217          ],
218        ],
219        'publish' => [
220          'label' => 'Publish',
221          'to' => 'published',
222          'weight' => 1,
223          'from' => [
224            'draft',
225            'published',
226          ],
227        ],
228      ],
229      'entity_types' => [],
230    ];
231  }
232
233  /**
234   * {@inheritdoc}
235   */
236  public function calculateDependencies() {
237    $dependencies = parent::calculateDependencies();
238    foreach ($this->getEntityTypes() as $entity_type_id) {
239      $entity_definition = $this->entityTypeManager->getDefinition($entity_type_id);
240      foreach ($this->getBundlesForEntityType($entity_type_id) as $bundle) {
241        $dependency = $entity_definition->getBundleConfigDependency($bundle);
242        $dependencies[$dependency['type']][] = $dependency['name'];
243      }
244    }
245    return $dependencies;
246  }
247
248  /**
249   * {@inheritdoc}
250   */
251  public function onDependencyRemoval(array $dependencies) {
252    $changed = parent::onDependencyRemoval($dependencies);
253
254    // When bundle config entities are removed, ensure they are cleaned up from
255    // the workflow.
256    foreach ($dependencies['config'] as $removed_config) {
257      if ($entity_type_id = $removed_config->getEntityType()->getBundleOf()) {
258        $bundle_id = $removed_config->id();
259        $this->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
260        $changed = TRUE;
261      }
262    }
263
264    // When modules that provide entity types are removed, ensure they are also
265    // removed from the workflow.
266    if (!empty($dependencies['module'])) {
267      // Gather all entity definitions provided by the dependent modules which
268      // are being removed.
269      $module_entity_definitions = [];
270      foreach ($this->entityTypeManager->getDefinitions() as $entity_definition) {
271        if (in_array($entity_definition->getProvider(), $dependencies['module'])) {
272          $module_entity_definitions[] = $entity_definition;
273        }
274      }
275
276      // For all entity types provided by the uninstalled modules, remove any
277      // configuration for those types.
278      foreach ($module_entity_definitions as $module_entity_definition) {
279        foreach ($this->getBundlesForEntityType($module_entity_definition->id()) as $bundle) {
280          $this->removeEntityTypeAndBundle($module_entity_definition->id(), $bundle);
281          $changed = TRUE;
282        }
283      }
284    }
285
286    return $changed;
287  }
288
289  /**
290   * {@inheritdoc}
291   */
292  public function getConfiguration() {
293    $configuration = parent::getConfiguration();
294    // Ensure that states and entity types are ordered consistently.
295    ksort($configuration['states']);
296    ksort($configuration['entity_types']);
297    return $configuration;
298  }
299
300  /**
301   * {@inheritdoc}
302   */
303  public function getInitialState($entity = NULL) {
304    // Workflows are not tied to entities, but Content Moderation adds the
305    // relationship between Workflows and entities. Content Moderation needs the
306    // entity object to be able to determine the initial state based on
307    // publishing status.
308    if (!($entity instanceof ContentEntityInterface)) {
309      throw new \InvalidArgumentException('A content entity object must be supplied.');
310    }
311    if ($entity instanceof EntityPublishedInterface && !$entity->isNew()) {
312      return $this->getState($entity->isPublished() ? 'published' : 'draft');
313    }
314
315    return $this->getState(!empty($this->configuration['default_moderation_state']) ? $this->configuration['default_moderation_state'] : 'draft');
316  }
317
318}
319