3 namespace Drupal\content_moderation;
5 use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
6 use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
7 use Drupal\Core\Entity\BundleEntityFormBase;
8 use Drupal\Core\Entity\ContentEntityFormInterface;
9 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
10 use Drupal\Core\Entity\ContentEntityTypeInterface;
11 use Drupal\Core\Entity\EntityInterface;
12 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
13 use Drupal\Core\Entity\EntityTypeInterface;
14 use Drupal\Core\Entity\EntityTypeManagerInterface;
15 use Drupal\Core\Field\BaseFieldDefinition;
16 use Drupal\Core\Form\FormStateInterface;
17 use Drupal\Core\Session\AccountInterface;
18 use Drupal\Core\StringTranslation\StringTranslationTrait;
19 use Drupal\Core\StringTranslation\TranslationInterface;
21 use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
22 use Drupal\content_moderation\Entity\Handler\ModerationHandler;
23 use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
24 use Drupal\content_moderation\Form\BundleModerationConfigurationForm;
25 use Drupal\content_moderation\Routing\EntityModerationRouteProvider;
26 use Drupal\content_moderation\Routing\EntityTypeModerationRouteProvider;
27 use Symfony\Component\DependencyInjection\ContainerInterface;
30 * Manipulates entity type information.
32 * This class contains primarily bridged hooks for compile-time or
33 * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
35 class EntityTypeInfo implements ContainerInjectionInterface {
37 use StringTranslationTrait;
40 * The moderation information service.
42 * @var \Drupal\content_moderation\ModerationInformationInterface
44 protected $moderationInfo;
47 * The entity type manager.
49 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
51 protected $entityTypeManager;
54 * The bundle information service.
56 * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
58 protected $bundleInfo;
63 * @var \Drupal\Core\Session\AccountInterface
65 protected $currentUser;
68 * A keyed array of custom moderation handlers for given entity types.
70 * Any entity not specified will use a common default.
74 protected $moderationHandlers = [
75 'node' => NodeModerationHandler::class,
76 'block_content' => BlockContentModerationHandler::class,
80 * EntityTypeInfo constructor.
82 * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
83 * The translation service. for form alters.
84 * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
85 * The moderation information service.
86 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
87 * Entity type manager.
88 * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
89 * Bundle information service.
90 * @param \Drupal\Core\Session\AccountInterface $current_user
93 public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user) {
94 $this->stringTranslation = $translation;
95 $this->moderationInfo = $moderation_information;
96 $this->entityTypeManager = $entity_type_manager;
97 $this->bundleInfo = $bundle_info;
98 $this->currentUser = $current_user;
104 public static function create(ContainerInterface $container) {
106 $container->get('string_translation'),
107 $container->get('content_moderation.moderation_information'),
108 $container->get('entity_type.manager'),
109 $container->get('entity_type.bundle.info'),
110 $container->get('current_user')
116 * Adds Moderation configuration to appropriate entity types.
118 * This is an alter hook bridge.
120 * @param EntityTypeInterface[] $entity_types
121 * The master entity type list to alter.
123 * @see hook_entity_type_alter()
125 public function entityTypeAlter(array &$entity_types) {
126 foreach ($entity_types as $entity_type_id => $entity_type) {
127 // The ContentModerationState entity type should never be moderated.
128 if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
129 $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
130 // Add additional moderation support to entity types whose bundles are
131 // managed by a config entity type.
132 if ($entity_type->getBundleEntityType()) {
133 $entity_types[$entity_type->getBundleEntityType()] = $this->addModerationToBundleEntityType($entity_types[$entity_type->getBundleEntityType()]);
140 * Modifies an entity definition to include moderation support.
142 * This primarily just means an extra handler. A Generic one is provided,
143 * but individual entity types can provide their own as appropriate.
145 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
146 * The content entity definition to modify.
148 * @return \Drupal\Core\Entity\ContentEntityTypeInterface
149 * The modified content entity definition.
151 protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
152 if (!$type->hasHandlerClass('moderation')) {
153 $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
154 $type->setHandlerClass('moderation', $handler_class);
157 if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
158 $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
161 // @todo Core forgot to add a direct way to manipulate route_provider, so
162 // we have to do it the sloppy way for now.
163 $providers = $type->getRouteProviderClasses() ?: [];
164 if (empty($providers['moderation'])) {
165 $providers['moderation'] = EntityModerationRouteProvider::class;
166 $type->setHandlerClass('route_provider', $providers);
173 * Configures moderation configuration support on a entity type definition.
175 * That "configuration support" includes a configuration form, a hypermedia
176 * link, and a route provider to tie it all together. There's also a
177 * moderation handler for per-entity-type variation.
179 * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $type
180 * The config entity definition to modify.
182 * @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface
183 * The modified config entity definition.
185 protected function addModerationToBundleEntityType(ConfigEntityTypeInterface $type) {
186 if ($type->hasLinkTemplate('edit-form') && !$type->hasLinkTemplate('moderation-form')) {
187 $type->setLinkTemplate('moderation-form', $type->getLinkTemplate('edit-form') . '/moderation');
190 if (!$type->getFormClass('moderation')) {
191 $type->setFormClass('moderation', BundleModerationConfigurationForm::class);
194 // @todo Core forgot to add a direct way to manipulate route_provider, so
195 // we have to do it the sloppy way for now.
196 $providers = $type->getRouteProviderClasses() ?: [];
197 if (empty($providers['moderation'])) {
198 $providers['moderation'] = EntityTypeModerationRouteProvider::class;
199 $type->setHandlerClass('route_provider', $providers);
206 * Adds an operation on bundles that should have a Moderation form.
208 * @param \Drupal\Core\Entity\EntityInterface $entity
209 * The entity on which to define an operation.
212 * An array of operation definitions.
214 * @see hook_entity_operation()
216 public function entityOperation(EntityInterface $entity) {
218 $type = $entity->getEntityType();
219 $bundle_of = $type->getBundleOf();
220 if ($this->currentUser->hasPermission('administer content moderation') && $bundle_of &&
221 $this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of))
223 $operations['manage-moderation'] = [
224 'title' => t('Manage moderation'),
226 'url' => Url::fromRoute("entity.{$type->id()}.moderation", [$entity->getEntityTypeId() => $entity->id()]),
234 * Gets the "extra fields" for a bundle.
236 * This is a hook bridge.
238 * @see hook_entity_extra_field_info()
241 * A nested array of 'pseudo-field' elements. Each list is nested within the
242 * following keys: entity type, bundle name, context (either 'form' or
243 * 'display'). The keys are the name of the elements as appearing in the
244 * renderable array (either the entity form or the displayed entity). The
245 * value is an associative array:
246 * - label: The human readable name of the element. Make sure you sanitize
247 * this appropriately.
248 * - description: A short description of the element contents.
249 * - weight: The default weight of the element.
250 * - visible: (optional) The default visibility of the element. Defaults to
252 * - edit: (optional) String containing markup (normally a link) used as the
253 * element's 'edit' operation in the administration interface. Only for
255 * - delete: (optional) String containing markup (normally a link) used as
256 * the element's 'delete' operation in the administration interface. Only
257 * for 'form' context.
259 public function entityExtraFieldInfo() {
261 foreach ($this->getModeratedBundles() as $bundle) {
262 $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
263 'label' => $this->t('Moderation control'),
264 'description' => $this->t("Status listing and form for the entity's moderation state."),
274 * Returns an iterable list of entity names and bundle names under moderation.
276 * That is, this method returns a list of bundles that have Content
277 * Moderation enabled on them.
280 * A generator, yielding a 2 element associative array:
281 * - entity: The machine name of an entity type, such as "node" or
283 * - bundle: The machine name of a bundle, such as "page" or "article".
285 protected function getModeratedBundles() {
286 $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
287 foreach ($entity_types as $type_name => $type) {
288 foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
289 if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
290 yield ['entity' => $type_name, 'bundle' => $bundle_id];
297 * Adds base field info to an entity type.
299 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
300 * Entity type for adding base fields to.
302 * @return \Drupal\Core\Field\BaseFieldDefinition[]
303 * New fields added by moderation state.
305 public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
306 if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
311 $fields['moderation_state'] = BaseFieldDefinition::create('string')
312 ->setLabel(t('Moderation state'))
313 ->setDescription(t('The moderation state of this piece of content.'))
315 ->setClass(ModerationStateFieldItemList::class)
316 ->setSetting('target_type', 'moderation_state')
317 ->setDisplayOptions('view', [
319 'region' => 'hidden',
322 ->setDisplayOptions('form', [
323 'type' => 'moderation_state_default',
327 ->addConstraint('ModerationState', [])
328 ->setDisplayConfigurable('form', FALSE)
329 ->setDisplayConfigurable('view', FALSE)
331 ->setTranslatable(TRUE);
337 * Alters bundle forms to enforce revision handling.
340 * An associative array containing the structure of the form.
341 * @param \Drupal\Core\Form\FormStateInterface $form_state
342 * The current state of the form.
343 * @param string $form_id
346 * @see hook_form_alter()
348 public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
349 $form_object = $form_state->getFormObject();
350 if ($form_object instanceof BundleEntityFormBase) {
351 $type = $form_object->getEntity()->getEntityType();
352 if ($this->moderationInfo->canModerateEntitiesOfEntityType($type)) {
353 $this->entityTypeManager->getHandler($type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
356 elseif ($form_object instanceof ContentEntityFormInterface) {
357 $entity = $form_object->getEntity();
358 if ($this->moderationInfo->isModeratedEntity($entity)) {
359 $this->entityTypeManager
360 ->getHandler($entity->getEntityTypeId(), 'moderation')
361 ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
362 // Submit handler to redirect to the latest version, if available.
363 $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
369 * Redirect content entity edit forms on save, if there is a forward revision.
371 * When saving their changes, editors should see those changes displayed on
375 * An associative array containing the structure of the form.
376 * @param \Drupal\Core\Form\FormStateInterface $form_state
377 * The current state of the form.
379 public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
380 /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
381 $entity = $form_state->getFormObject()->getEntity();
383 $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
384 if ($moderation_info->hasForwardRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
385 $entity_type_id = $entity->getEntityTypeId();
386 $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);