3 namespace Drupal\content_moderation;
5 use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
6 use Drupal\Core\Entity\BundleEntityFormBase;
7 use Drupal\Core\Entity\ContentEntityFormInterface;
8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9 use Drupal\Core\Entity\ContentEntityTypeInterface;
10 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
11 use Drupal\Core\Entity\EntityTypeInterface;
12 use Drupal\Core\Entity\EntityTypeManagerInterface;
13 use Drupal\Core\Field\BaseFieldDefinition;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\Session\AccountInterface;
16 use Drupal\Core\StringTranslation\StringTranslationTrait;
17 use Drupal\Core\StringTranslation\TranslationInterface;
18 use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
19 use Drupal\content_moderation\Entity\Handler\ModerationHandler;
20 use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
21 use Drupal\content_moderation\Entity\Routing\EntityModerationRouteProvider;
22 use Symfony\Component\DependencyInjection\ContainerInterface;
25 * Manipulates entity type information.
27 * This class contains primarily bridged hooks for compile-time or
28 * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
32 class EntityTypeInfo implements ContainerInjectionInterface {
34 use StringTranslationTrait;
37 * The moderation information service.
39 * @var \Drupal\content_moderation\ModerationInformationInterface
41 protected $moderationInfo;
44 * The entity type manager.
46 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
48 protected $entityTypeManager;
51 * The bundle information service.
53 * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
55 protected $bundleInfo;
60 * @var \Drupal\Core\Session\AccountInterface
62 protected $currentUser;
65 * The state transition validation service.
67 * @var \Drupal\content_moderation\StateTransitionValidationInterface
72 * A keyed array of custom moderation handlers for given entity types.
74 * Any entity not specified will use a common default.
78 protected $moderationHandlers = [
79 'node' => NodeModerationHandler::class,
80 'block_content' => BlockContentModerationHandler::class,
84 * EntityTypeInfo constructor.
86 * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
87 * The translation service. for form alters.
88 * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
89 * The moderation information service.
90 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
91 * Entity type manager.
92 * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
93 * Bundle information service.
94 * @param \Drupal\Core\Session\AccountInterface $current_user
97 public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user, StateTransitionValidationInterface $validator) {
98 $this->stringTranslation = $translation;
99 $this->moderationInfo = $moderation_information;
100 $this->entityTypeManager = $entity_type_manager;
101 $this->bundleInfo = $bundle_info;
102 $this->currentUser = $current_user;
103 $this->validator = $validator;
109 public static function create(ContainerInterface $container) {
111 $container->get('string_translation'),
112 $container->get('content_moderation.moderation_information'),
113 $container->get('entity_type.manager'),
114 $container->get('entity_type.bundle.info'),
115 $container->get('current_user'),
116 $container->get('content_moderation.state_transition_validation')
122 * Adds Moderation configuration to appropriate entity types.
124 * @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
125 * The master entity type list to alter.
127 * @see hook_entity_type_alter()
129 public function entityTypeAlter(array &$entity_types) {
130 foreach ($entity_types as $entity_type_id => $entity_type) {
131 // The ContentModerationState entity type should never be moderated.
132 if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
133 $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
139 * Modifies an entity definition to include moderation support.
141 * This primarily just means an extra handler. A Generic one is provided,
142 * but individual entity types can provide their own as appropriate.
144 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
145 * The content entity definition to modify.
147 * @return \Drupal\Core\Entity\ContentEntityTypeInterface
148 * The modified content entity definition.
150 protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
151 if (!$type->hasHandlerClass('moderation')) {
152 $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
153 $type->setHandlerClass('moderation', $handler_class);
156 if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
157 $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
160 $providers = $type->getRouteProviderClasses() ?: [];
161 if (empty($providers['moderation'])) {
162 $providers['moderation'] = EntityModerationRouteProvider::class;
163 $type->setHandlerClass('route_provider', $providers);
170 * Gets the "extra fields" for a bundle.
173 * A nested array of 'pseudo-field' elements. Each list is nested within the
174 * following keys: entity type, bundle name, context (either 'form' or
175 * 'display'). The keys are the name of the elements as appearing in the
176 * renderable array (either the entity form or the displayed entity). The
177 * value is an associative array:
178 * - label: The human readable name of the element. Make sure you sanitize
179 * this appropriately.
180 * - description: A short description of the element contents.
181 * - weight: The default weight of the element.
182 * - visible: (optional) The default visibility of the element. Defaults to
184 * - edit: (optional) String containing markup (normally a link) used as the
185 * element's 'edit' operation in the administration interface. Only for
187 * - delete: (optional) String containing markup (normally a link) used as
188 * the element's 'delete' operation in the administration interface. Only
189 * for 'form' context.
191 * @see hook_entity_extra_field_info()
193 public function entityExtraFieldInfo() {
195 foreach ($this->getModeratedBundles() as $bundle) {
196 $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
197 'label' => $this->t('Moderation control'),
198 'description' => $this->t("Status listing and form for the entity's moderation state."),
208 * Returns an iterable list of entity names and bundle names under moderation.
210 * That is, this method returns a list of bundles that have Content
211 * Moderation enabled on them.
214 * A generator, yielding a 2 element associative array:
215 * - entity: The machine name of an entity type, such as "node" or
217 * - bundle: The machine name of a bundle, such as "page" or "article".
219 protected function getModeratedBundles() {
220 $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
221 foreach ($entity_types as $type_name => $type) {
222 foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
223 if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
224 yield ['entity' => $type_name, 'bundle' => $bundle_id];
231 * Adds base field info to an entity type.
233 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
234 * Entity type for adding base fields to.
236 * @return \Drupal\Core\Field\BaseFieldDefinition[]
237 * New fields added by moderation state.
239 * @see hook_entity_base_field_info()
241 public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
242 if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
247 $fields['moderation_state'] = BaseFieldDefinition::create('string')
248 ->setLabel(t('Moderation state'))
249 ->setDescription(t('The moderation state of this piece of content.'))
251 ->setClass(ModerationStateFieldItemList::class)
252 ->setDisplayOptions('view', [
254 'region' => 'hidden',
257 ->setDisplayOptions('form', [
258 'type' => 'moderation_state_default',
262 ->addConstraint('ModerationState', [])
263 ->setDisplayConfigurable('form', TRUE)
264 ->setDisplayConfigurable('view', FALSE)
266 ->setTranslatable(TRUE);
272 * Alters bundle forms to enforce revision handling.
275 * An associative array containing the structure of the form.
276 * @param \Drupal\Core\Form\FormStateInterface $form_state
277 * The current state of the form.
278 * @param string $form_id
281 * @see hook_form_alter()
283 public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
284 $form_object = $form_state->getFormObject();
285 if ($form_object instanceof BundleEntityFormBase) {
286 $config_entity_type = $form_object->getEntity()->getEntityType();
287 $bundle_of = $config_entity_type->getBundleOf();
289 && ($bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle_of))
290 && $this->moderationInfo->canModerateEntitiesOfEntityType($bundle_of_entity_type)) {
291 $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
294 elseif ($form_object instanceof ContentEntityFormInterface && in_array($form_object->getOperation(), ['edit', 'default'])) {
295 $entity = $form_object->getEntity();
296 if ($this->moderationInfo->isModeratedEntity($entity)) {
297 $this->entityTypeManager
298 ->getHandler($entity->getEntityTypeId(), 'moderation')
299 ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
301 if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) {
302 $latest_revision = $this->moderationInfo->getLatestRevision($entity->getEntityTypeId(), $entity->id());
303 if ($entity->bundle()) {
304 $bundle_type_id = $entity->getEntityType()->getBundleEntityType();
305 $bundle = $this->entityTypeManager->getStorage($bundle_type_id)->load($entity->bundle());
306 $type_label = $bundle->label();
309 $type_label = $entity->getEntityType()->getLabel();
312 $translation = $this->moderationInfo->getAffectedRevisionTranslation($latest_revision);
314 '@type_label' => $type_label,
315 '@latest_revision_edit_url' => $translation->toUrl('edit-form', ['language' => $translation->language()])->toString(),
316 '@latest_revision_delete_url' => $translation->toUrl('delete-form', ['language' => $translation->language()])->toString(),
318 $label = $this->t('Unable to save this @type_label.', $args);
319 $message = $this->t('<a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
320 $full_message = $this->t('Unable to save this @type_label. <a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
321 drupal_set_message($full_message, 'error');
323 $form['moderation_state']['#access'] = FALSE;
324 $form['actions']['#access'] = FALSE;
325 $form['invalid_transitions'] = [
328 '#prefix' => '<strong class="label">',
330 '#suffix' => '</strong>',
334 '#markup' => $message,
337 '#no_valid_transitions' => TRUE,
340 if ($form['footer']) {
341 $form['invalid_transitions']['#group'] = 'footer';
345 // Submit handler to redirect to the latest version, if available.
346 $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
348 // Move the 'moderation_state' field widget to the footer region, if
350 if (isset($form['footer'])) {
351 $form['moderation_state']['#group'] = 'footer';
354 // Duplicate the label of the current moderation state to the meta
355 // region, if available.
356 if (isset($form['meta']['published'])) {
357 $form['meta']['published']['#markup'] = $form['moderation_state']['widget'][0]['current']['#markup'];
364 * Redirect content entity edit forms on save, if there is a pending revision.
366 * When saving their changes, editors should see those changes displayed on
370 * An associative array containing the structure of the form.
371 * @param \Drupal\Core\Form\FormStateInterface $form_state
372 * The current state of the form.
374 public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
375 /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
376 $entity = $form_state->getFormObject()->getEntity();
378 $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
379 if ($moderation_info->hasPendingRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
380 $entity_type_id = $entity->getEntityTypeId();
381 $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);