EntityTypeInfo.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. namespace Drupal\content_moderation;
  3. use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
  4. use Drupal\Core\Entity\BundleEntityFormBase;
  5. use Drupal\Core\Entity\ContentEntityFormInterface;
  6. use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
  7. use Drupal\Core\Entity\ContentEntityTypeInterface;
  8. use Drupal\Core\Entity\EntityInterface;
  9. use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
  10. use Drupal\Core\Entity\EntityTypeInterface;
  11. use Drupal\Core\Entity\EntityTypeManagerInterface;
  12. use Drupal\Core\Field\BaseFieldDefinition;
  13. use Drupal\Core\Form\FormInterface;
  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;
  23. /**
  24. * Manipulates entity type information.
  25. *
  26. * This class contains primarily bridged hooks for compile-time or
  27. * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
  28. *
  29. * @internal
  30. */
  31. class EntityTypeInfo implements ContainerInjectionInterface {
  32. use StringTranslationTrait;
  33. /**
  34. * The moderation information service.
  35. *
  36. * @var \Drupal\content_moderation\ModerationInformationInterface
  37. */
  38. protected $moderationInfo;
  39. /**
  40. * The entity type manager.
  41. *
  42. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  43. */
  44. protected $entityTypeManager;
  45. /**
  46. * The bundle information service.
  47. *
  48. * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
  49. */
  50. protected $bundleInfo;
  51. /**
  52. * The current user.
  53. *
  54. * @var \Drupal\Core\Session\AccountInterface
  55. */
  56. protected $currentUser;
  57. /**
  58. * The state transition validation service.
  59. *
  60. * @var \Drupal\content_moderation\StateTransitionValidationInterface
  61. */
  62. protected $validator;
  63. /**
  64. * A keyed array of custom moderation handlers for given entity types.
  65. *
  66. * Any entity not specified will use a common default.
  67. *
  68. * @var array
  69. */
  70. protected $moderationHandlers = [
  71. 'node' => NodeModerationHandler::class,
  72. 'block_content' => BlockContentModerationHandler::class,
  73. ];
  74. /**
  75. * EntityTypeInfo constructor.
  76. *
  77. * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
  78. * The translation service. for form alters.
  79. * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
  80. * The moderation information service.
  81. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  82. * Entity type manager.
  83. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
  84. * Bundle information service.
  85. * @param \Drupal\Core\Session\AccountInterface $current_user
  86. * Current user.
  87. */
  88. public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user, StateTransitionValidationInterface $validator) {
  89. $this->stringTranslation = $translation;
  90. $this->moderationInfo = $moderation_information;
  91. $this->entityTypeManager = $entity_type_manager;
  92. $this->bundleInfo = $bundle_info;
  93. $this->currentUser = $current_user;
  94. $this->validator = $validator;
  95. }
  96. /**
  97. * {@inheritdoc}
  98. */
  99. public static function create(ContainerInterface $container) {
  100. return new static(
  101. $container->get('string_translation'),
  102. $container->get('content_moderation.moderation_information'),
  103. $container->get('entity_type.manager'),
  104. $container->get('entity_type.bundle.info'),
  105. $container->get('current_user'),
  106. $container->get('content_moderation.state_transition_validation')
  107. );
  108. }
  109. /**
  110. * Adds Moderation configuration to appropriate entity types.
  111. *
  112. * @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
  113. * The master entity type list to alter.
  114. *
  115. * @see hook_entity_type_alter()
  116. */
  117. public function entityTypeAlter(array &$entity_types) {
  118. foreach ($entity_types as $entity_type_id => $entity_type) {
  119. // The ContentModerationState entity type should never be moderated.
  120. if ($entity_type->isRevisionable() && !$entity_type->isInternal()) {
  121. $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
  122. }
  123. }
  124. }
  125. /**
  126. * Modifies an entity definition to include moderation support.
  127. *
  128. * This primarily just means an extra handler. A Generic one is provided,
  129. * but individual entity types can provide their own as appropriate.
  130. *
  131. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
  132. * The content entity definition to modify.
  133. *
  134. * @return \Drupal\Core\Entity\ContentEntityTypeInterface
  135. * The modified content entity definition.
  136. */
  137. protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
  138. if (!$type->hasHandlerClass('moderation')) {
  139. $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
  140. $type->setHandlerClass('moderation', $handler_class);
  141. }
  142. if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
  143. $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
  144. }
  145. $providers = $type->getRouteProviderClasses() ?: [];
  146. if (empty($providers['moderation'])) {
  147. $providers['moderation'] = EntityModerationRouteProvider::class;
  148. $type->setHandlerClass('route_provider', $providers);
  149. }
  150. return $type;
  151. }
  152. /**
  153. * Gets the "extra fields" for a bundle.
  154. *
  155. * @return array
  156. * A nested array of 'pseudo-field' elements. Each list is nested within the
  157. * following keys: entity type, bundle name, context (either 'form' or
  158. * 'display'). The keys are the name of the elements as appearing in the
  159. * renderable array (either the entity form or the displayed entity). The
  160. * value is an associative array:
  161. * - label: The human readable name of the element. Make sure you sanitize
  162. * this appropriately.
  163. * - description: A short description of the element contents.
  164. * - weight: The default weight of the element.
  165. * - visible: (optional) The default visibility of the element. Defaults to
  166. * TRUE.
  167. * - edit: (optional) String containing markup (normally a link) used as the
  168. * element's 'edit' operation in the administration interface. Only for
  169. * 'form' context.
  170. * - delete: (optional) String containing markup (normally a link) used as
  171. * the element's 'delete' operation in the administration interface. Only
  172. * for 'form' context.
  173. *
  174. * @see hook_entity_extra_field_info()
  175. */
  176. public function entityExtraFieldInfo() {
  177. $return = [];
  178. foreach ($this->getModeratedBundles() as $bundle) {
  179. $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
  180. 'label' => $this->t('Moderation control'),
  181. 'description' => $this->t("Status listing and form for the entity's moderation state."),
  182. 'weight' => -20,
  183. 'visible' => TRUE,
  184. ];
  185. }
  186. return $return;
  187. }
  188. /**
  189. * Returns an iterable list of entity names and bundle names under moderation.
  190. *
  191. * That is, this method returns a list of bundles that have Content
  192. * Moderation enabled on them.
  193. *
  194. * @return \Generator
  195. * A generator, yielding a 2 element associative array:
  196. * - entity: The machine name of an entity type, such as "node" or
  197. * "block_content".
  198. * - bundle: The machine name of a bundle, such as "page" or "article".
  199. */
  200. protected function getModeratedBundles() {
  201. $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
  202. foreach ($entity_types as $type_name => $type) {
  203. foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
  204. if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
  205. yield ['entity' => $type_name, 'bundle' => $bundle_id];
  206. }
  207. }
  208. }
  209. }
  210. /**
  211. * Adds base field info to an entity type.
  212. *
  213. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  214. * Entity type for adding base fields to.
  215. *
  216. * @return \Drupal\Core\Field\BaseFieldDefinition[]
  217. * New fields added by moderation state.
  218. *
  219. * @see hook_entity_base_field_info()
  220. */
  221. public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
  222. if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
  223. return [];
  224. }
  225. $fields = [];
  226. $fields['moderation_state'] = BaseFieldDefinition::create('string')
  227. ->setLabel(t('Moderation state'))
  228. ->setDescription(t('The moderation state of this piece of content.'))
  229. ->setComputed(TRUE)
  230. ->setClass(ModerationStateFieldItemList::class)
  231. ->setDisplayOptions('view', [
  232. 'label' => 'hidden',
  233. 'region' => 'hidden',
  234. 'weight' => -5,
  235. ])
  236. ->setDisplayOptions('form', [
  237. 'type' => 'moderation_state_default',
  238. 'weight' => 100,
  239. 'settings' => [],
  240. ])
  241. ->addConstraint('ModerationState', [])
  242. ->setDisplayConfigurable('form', TRUE)
  243. ->setDisplayConfigurable('view', FALSE)
  244. ->setReadOnly(FALSE)
  245. ->setTranslatable(TRUE);
  246. return $fields;
  247. }
  248. /**
  249. * Replaces the entity form entity object with a proper revision object.
  250. *
  251. * @param \Drupal\Core\Entity\EntityInterface $entity
  252. * The entity being edited.
  253. * @param string $operation
  254. * The entity form operation.
  255. * @param \Drupal\Core\Form\FormStateInterface $form_state
  256. * The form state.
  257. *
  258. * @see hook_entity_prepare_form()
  259. */
  260. public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
  261. /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
  262. $form_object = $form_state->getFormObject();
  263. if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) {
  264. // Generate a proper revision object for the current entity. This allows
  265. // to correctly handle translatable entities having pending revisions.
  266. /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
  267. $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
  268. /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
  269. $new_revision = $storage->createRevision($entity, FALSE);
  270. // Restore the revision ID as other modules may expect to find it still
  271. // populated. This will reset the "new revision" flag, however the entity
  272. // object will be marked as a new revision again on submit.
  273. // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
  274. $revision_key = $new_revision->getEntityType()->getKey('revision');
  275. $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
  276. $form_object->setEntity($new_revision);
  277. }
  278. }
  279. /**
  280. * Alters bundle forms to enforce revision handling.
  281. *
  282. * @param array $form
  283. * An associative array containing the structure of the form.
  284. * @param \Drupal\Core\Form\FormStateInterface $form_state
  285. * The current state of the form.
  286. * @param string $form_id
  287. * The form id.
  288. *
  289. * @see hook_form_alter()
  290. */
  291. public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
  292. $form_object = $form_state->getFormObject();
  293. if ($form_object instanceof BundleEntityFormBase) {
  294. $config_entity_type = $form_object->getEntity()->getEntityType();
  295. $bundle_of = $config_entity_type->getBundleOf();
  296. if ($bundle_of
  297. && ($bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle_of))
  298. && $this->moderationInfo->canModerateEntitiesOfEntityType($bundle_of_entity_type)) {
  299. $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
  300. }
  301. }
  302. elseif ($this->isModeratedEntityEditForm($form_object)) {
  303. /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
  304. /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
  305. $entity = $form_object->getEntity();
  306. if ($this->moderationInfo->isModeratedEntity($entity)) {
  307. $this->entityTypeManager
  308. ->getHandler($entity->getEntityTypeId(), 'moderation')
  309. ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
  310. // Submit handler to redirect to the latest version, if available.
  311. $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
  312. // Move the 'moderation_state' field widget to the footer region, if
  313. // available.
  314. if (isset($form['footer'])) {
  315. $form['moderation_state']['#group'] = 'footer';
  316. }
  317. // If the publishing status exists in the meta region, replace it with
  318. // the current state instead.
  319. if (isset($form['meta']['published'])) {
  320. $form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label();
  321. }
  322. }
  323. }
  324. }
  325. /**
  326. * Checks whether the specified form allows to edit a moderated entity.
  327. *
  328. * @param \Drupal\Core\Form\FormInterface $form_object
  329. * The form object.
  330. *
  331. * @return bool
  332. * TRUE if the form should get form moderation, FALSE otherwise.
  333. */
  334. protected function isModeratedEntityEditForm(FormInterface $form_object) {
  335. return $form_object instanceof ContentEntityFormInterface &&
  336. in_array($form_object->getOperation(), ['edit', 'default'], TRUE) &&
  337. $this->moderationInfo->isModeratedEntity($form_object->getEntity());
  338. }
  339. /**
  340. * Redirect content entity edit forms on save, if there is a pending revision.
  341. *
  342. * When saving their changes, editors should see those changes displayed on
  343. * the next page.
  344. *
  345. * @param array $form
  346. * An associative array containing the structure of the form.
  347. * @param \Drupal\Core\Form\FormStateInterface $form_state
  348. * The current state of the form.
  349. */
  350. public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
  351. /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
  352. $entity = $form_state->getFormObject()->getEntity();
  353. $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
  354. if ($moderation_info->hasPendingRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
  355. $entity_type_id = $entity->getEntityTypeId();
  356. $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);
  357. }
  358. }
  359. }