diff --git a/moderation_state.install b/moderation_state.install index 6a4b978..7478667 100644 --- a/moderation_state.install +++ b/moderation_state.install @@ -5,10 +5,19 @@ * Contains install/update hooks for moderation_state. */ +use Drupal\Core\Entity\EntityTypeInterface; + /** * Implements hook_install(). */ function moderation_state_install() { - $moderation_state_definition = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node')['moderation_state']; - \Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition('moderation_state', 'node', 'moderation_state', $moderation_state_definition); + + $content_entity_type_ids = array_keys(array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->isRevisionable(); + })); + + foreach ($content_entity_type_ids as $content_entity_type_id) { + $moderation_state_definition = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($content_entity_type_id)['moderation_state']; + \Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition('moderation_state', $content_entity_type_id, 'moderation_state', $moderation_state_definition); + } } diff --git a/moderation_state.links.task.yml b/moderation_state.links.task.yml new file mode 100644 index 0000000..4f62fd7 --- /dev/null +++ b/moderation_state.links.task.yml @@ -0,0 +1,3 @@ +moderation_state.entities: + deriver: 'Drupal\moderation_state\Plugin\Derivative\DynamicLocalTasks' + weight: 100 diff --git a/moderation_state.module b/moderation_state.module index 32c30ca..5a834ec 100644 --- a/moderation_state.module +++ b/moderation_state.module @@ -9,16 +9,12 @@ * revision) - i.e. unpublish */ +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\moderation_state\BaseFieldInfo; -use Drupal\moderation_state\NodeEventSubscriber; use Drupal\moderation_state\Plugin\Menu\EditTab; -use Drupal\node\Entity\NodeType; -use Drupal\node\NodeInterface; -use Drupal\node\NodeTypeInterface; +use Drupal\Core\Form\FormStateInterface; /** * Implements hook_help(). @@ -45,126 +41,52 @@ function moderation_state_entity_base_field_info(EntityTypeInterface $entity_typ } /** - * Implements hook_form_FORM_ID_alter() for node_type_form(). + * Implements hook_entity_type_alter(). */ -function moderation_state_form_node_type_form_alter(&$form, FormStateInterface $form_state) { - // @todo write a test for this. - /* @var NodeTypeInterface $node_type */ - $node_type = $form_state->getFormObject()->getEntity(); - $form['workflow']['enable_moderation_state'] = [ - '#type' => 'checkbox', - '#title' => t('Enable moderation states.'), - '#description' => t('Content of this type must transition through moderation states in order to be published.'), - '#default_value' => $node_type->getThirdPartySetting('moderation_state', 'enabled', FALSE), - ]; - $states = \Drupal::entityTypeManager()->getStorage('moderation_state')->loadMultiple(); - $options = []; - foreach ($states as $key => $state) { - $options[$key] = $state->label() . ' ' . ($state->isPublishedState() ? t('(published)') : t('(non-published)')); - } - $form['workflow']['allowed_moderation_states'] = [ - '#type' => 'checkboxes', - '#title' => t('Allowed moderation states.'), - '#description' => t('The allowed moderation states this content-type can be assigned. You must select at least one published and one non-published state.'), - '#default_value' => $node_type->getThirdPartySetting('moderation_state', 'allowed_moderation_states', []), - '#options' => $options, - '#states' => [ - 'visible' => [ - ':input[name=enable_moderation_state]' => ['checked' => TRUE], - ], - ], - ]; - $form['workflow']['default_moderation_state'] = [ - '#type' => 'select', - '#title' => t('Default moderation state'), - '#empty_option' => t('-- Select --'), - '#options' => $options, - '#description' => t('Select the moderation state for new content'), - '#default_value' => $node_type->getThirdPartySetting('moderation_state', 'default_moderation_state', ''), - '#states' => [ - 'visible' => [ - ':input[name=enable_moderation_state]' => ['checked' => TRUE], - ], - ], - ]; - $form['#entity_builders'][] = 'moderation_state_node_type_form_builder'; - $form['#validate'][] = 'moderation_state_node_type_form_validate'; +function moderation_state_entity_type_alter(array &$entity_types) { + \Drupal::service('moderation_state.entity_type')->entityTypeAlter($entity_types); } /** - * Validation function for node-type form. + * Implements hook_entity_operation(). */ -function moderation_state_node_type_form_validate(array $form, FormStateInterface $form_state) { - // @todo write a test for this. - if ($form_state->getValue('enable_moderation_state')) { - $states = \Drupal::entityTypeManager()->getStorage('moderation_state')->loadMultiple(); - $published = FALSE; - $non_published = TRUE; - $allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states'))); - foreach ($allowed as $state_id) { - $state = $states[$state_id]; - if ($state->isPublishedState()) { - $published = TRUE; - } - else { - $non_published = TRUE; - } - } - if (!$published || !$non_published) { - $form_state->setErrorByName('allowed_moderation_states', t('You must select at least one published moderation and one non-published state.')); - } - if (($default = $form_state->getValue('default_moderation_state')) && !empty($default)) { - if (!in_array($default, $allowed, TRUE)) { - $form_state->setErrorByName('default_moderation_state', t('The default moderation state must be one of the allowed states.')); - } - } - else { - $form_state->setErrorByName('default_moderation_state', t('You must select a default moderation state.')); - } - } -} - -/** - * Entity builder for the node type edit form with third party options. - * - * @see moderation_state_form_node_type_form_alter() - */ -function moderation_state_node_type_form_builder($entity_type, NodeTypeInterface $node_type, &$form, FormStateInterface $form_state) { - // @todo write a test for this. - $node_type->setThirdPartySetting('moderation_state', 'enabled', $form_state->getValue('enable_moderation_state')); - $node_type->setThirdPartySetting('moderation_state', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states')))); - $node_type->setThirdPartySetting('moderation_state', 'default_moderation_state', $form_state->getValue('default_moderation_state')); +function moderation_state_entity_operation(EntityInterface $entity) { + return \Drupal::service('moderation_state.entity_type')->entityOperation($entity); } /** * Sets required flag based on enabled state. */ function moderation_state_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) { - if ($entity_type->id() === 'node' && !empty($fields['moderation_state'])) { - /* @var NodeTypeInterface $node_type */ - $node_type = NodeType::load($bundle); - if ($node_type->getThirdPartySetting('moderation_state', 'enabled', FALSE)) { - /* @var \Drupal\Core\Field\FieldDefinitionInterface $field */ - // @todo write a test for this. - $fields['moderation_state']->addConstraint('ModerationState', []); - } - } + return \Drupal::service('moderation_state.entity_type')->entityBundleFieldInfoAlter($fields, $entity_type, $bundle); } /** - * Acts on a node and set the published status based on the moderation state. - * - * @param \Drupal\node\NodeInterface $node - * The node being saved. + * Implements hook_entity_presave(). */ -function moderation_state_node_presave(NodeInterface $node) { - $node_event_subscriber = new NodeEventSubscriber(\Drupal::entityTypeManager()); - $node_event_subscriber->nodePresave($node); +function moderation_state_entity_presave(EntityInterface $entity) { + return \Drupal::service('moderation_state.entity_operations')->entityPresave($entity); } /** * Implements hook_local_tasks_alter(). */ function moderation_state_local_tasks_alter(&$local_tasks) { - $local_tasks['entity.node.edit_form']['class'] = EditTab::class; + $content_entity_type_ids = array_keys(array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->isRevisionable(); + })); + + foreach ($content_entity_type_ids as $content_entity_type_id) { + if (isset($local_tasks["entity.$content_entity_type_id.edit_form"])) { + $local_tasks["entity.$content_entity_type_id.edit_form"]['class'] = EditTab::class; + $local_tasks["entity.$content_entity_type_id.edit_form"]['entity_type_id'] = $content_entity_type_id; + } + } +} + +/** + * Implements hook_form_alter(). + */ +function moderation_state_form_alter(&$form, FormStateInterface $form_state, $form_id) { + return \Drupal::service('moderation_state.entity_type')->bundleFormAlter($form, $form_state, $form_id); } diff --git a/moderation_state.services.yml b/moderation_state.services.yml index b8df092..78320c6 100644 --- a/moderation_state.services.yml +++ b/moderation_state.services.yml @@ -1,6 +1,17 @@ services: paramconverter.latest_revision: - class: Drupal\moderation_state\ParamConverter\NodeRevisionConverter + class: Drupal\moderation_state\ParamConverter\EntityRevisionConverter tags: - { name: paramconverter, priority: 5 } arguments: ['@entity.manager'] + moderation_state.moderation_information: + class: Drupal\moderation_state\ModerationInformation + tags: + arguments: ['@entity_type.manager', '@current_user'] + moderation_state.entity_type: + class: Drupal\moderation_state\EntityTypeInfo + tags: + arguments: ['@string_translation', '@moderation_state.moderation_information'] + moderation_state.entity_operations: + class: Drupal\moderation_state\EntityOperations + arguments: ['@moderation_state.moderation_information'] diff --git a/src/BaseFieldInfo.php b/src/BaseFieldInfo.php index f74503b..2da273f 100644 --- a/src/BaseFieldInfo.php +++ b/src/BaseFieldInfo.php @@ -16,13 +16,6 @@ use Drupal\Core\Field\BaseFieldDefinition; class BaseFieldInfo { /** - * Entity type ID to add fields for. - * - * @var string - */ - protected $targetEntityTypeId = 'node'; - - /** * Adds base field info to an entity type. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type @@ -32,13 +25,14 @@ class BaseFieldInfo { * New fields added by moderation state. */ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { - if ($entity_type->id() === $this->targetEntityTypeId) { + if ($entity_type->isRevisionable()) { $fields = []; // @todo write a test for this. $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Moderation state')) ->setDescription(t('The moderation state of this piece of content.')) ->setSetting('target_type', 'moderation_state') + ->setTargetEntityTypeId($entity_type->id()) ->setRevisionable(TRUE) // @todo write a test for this. ->setDisplayOptions('view', [ diff --git a/src/EntityOperations.php b/src/EntityOperations.php new file mode 100644 index 0000000..0f72094 --- /dev/null +++ b/src/EntityOperations.php @@ -0,0 +1,57 @@ +moderationInfo = $moderation_info; + } + + /** + * Acts on an entity and set the published status based on the moderation state. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being saved. + */ + public function entityPresave(EntityInterface $entity) { + if ($entity instanceof ContentEntityInterface && $this->moderationInfo->isModeratableEntity($entity)) { + // @todo write a test for this. + if ($entity->moderation_state->entity) { + // This is *probably* not necessary if configuration is setup correctly, + // but it can't hurt. + $entity->setNewRevision(TRUE); + $published_state = $entity->moderation_state->entity->isPublishedState(); + // A newly-created revision is always the default revision, or else + // it gets lost. + $entity->isDefaultRevision($entity->isNew() || $published_state); + $entity->setPublished($published_state); + } + } + } +} diff --git a/src/EntityTypeInfo.php b/src/EntityTypeInfo.php new file mode 100644 index 0000000..bf0497c --- /dev/null +++ b/src/EntityTypeInfo.php @@ -0,0 +1,205 @@ +stringTranslation = $translation; + $this->moderationInfo = $moderation_information; + } + + /** + * Adds Moderation configuration to appropriate entity types. + * + * This is an alter hook bridge. + * + * @see hook_entity_type_alter(). + * + * @param EntityTypeInterface[] $entity_types + * The master entity type list to alter. + */ + public function entityTypeAlter(array &$entity_types) { + foreach ($this->moderationInfo->selectRevisionableEntityTypes($entity_types) as $type_name => $type) { + $entity_types[$type_name] = $this->addModeration($type); + } + } + + /** + * Modifies an entity type to include moderation configuration support. + * + * That "configuration support" includes a configuration form, a hypermedia + * link, and a route provider to tie it all together. + * + * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $type + * The config entity definition to modify. + * @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface + * The modified config entity definition. + */ + protected function addModeration(ConfigEntityTypeInterface $type) { + if ($type->hasLinkTemplate('edit-form')) { + $type->setLinkTemplate('moderation-form', $type->getLinkTemplate('edit-form') . '/moderation'); + } + + $type->setFormClass('moderation', EntityModerationForm::class); + + // @todo Core forgot to add a direct way to manipulate route_provider, so + // we have to do it the sloppy way for now. + $providers = $type->getHandlerClass('route_provider') ?: []; + $providers['moderation'] = ModerationRouteProvider::class; + $type->setHandlerClass('route_provider', $providers); + + return $type; + } + + /** + * Adds an operation on bundles that should have a Moderation form. + * + * @see hook_entity_operation(). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity on which to define an operation. + * + * @return array + * An array of operation definitions. + */ + public function entityOperation(EntityInterface $entity) { + $operations = []; + $type = $entity->getEntityType(); + + if ($this->moderationInfo->isBundleForModeratableEntity($entity)) { + $operations['manage-moderation'] = [ + 'title' => t('Manage moderation'), + 'weight' => 27, + 'url' => Url::fromRoute("entity.{$type->id()}.moderation", [$entity->getEntityTypeId() => $entity->id()]), + ]; + } + + return $operations; + } + + /** + * Force moderatable bundles to have a moderation_state field. + * + * @see hook_entity_bundle_field_info_alter(); + * + * @param array $fields + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * @param string $bundle + */ + public function entityBundleFieldInfoAlter(&$fields, EntityTypeInterface $entity_type, $bundle) { + if ($this->moderationInfo->isModeratableBundle($entity_type, $bundle) && !empty($fields['moderation_state'])) { + $fields['moderation_state']->addConstraint('ModerationState', []); + } + + return; + } + + /** + * Alters bundle forms to enforce revision handling. + * + * @see hook_form_alter() + */ + public function bundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) { + if ($this->moderationInfo->isRevisionableBundleForm($form_state->getFormObject())) { + $this->enforceRevisionsBundleFormAlter($form, $form_state, $form_id); + } + else if ($this->moderationInfo->isModeratedEntityForm($form_state->getFormObject())) { + $this->enforceRevisionsEntityFormAlter($form, $form_state, $form_id); + } + } + + /** + * Alters entity forms to enforce revision handling. + * + * Different entity types structure their forms completely differently, so + * there's seemingly no way to do this globally. Instead, we'll just hard + * code form changes for core's entity types. Suggestions for a better + * approach are welcome. + * + * @see hook_form_alter() + * + * @param $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param $form_id + */ + protected function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) { + $entity = $form_state->getFormObject()->getEntity(); + + if ($entity instanceof Node) { + $form['revision']['#disabled'] = TRUE; + $form['revision']['#default_value'] = TRUE; + $form['revision']['#description'] = $this->t('Revisions are required.'); + } + else if ($entity instanceof BlockContent) { + + $form['revision_information']['revision']['#default_value'] = TRUE; + $form['revision_information']['revision']['#disabled'] = TRUE; + $form['revision_information']['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.'); + } + } + + /** + * Alters bundle forms to enforce revision handling. + * + * Different entity types structure their forms completely differently, so + * there's seemingly no way to do this globally. Instead, we'll just hard + * code form changes for core's entity types. Suggestions for a better + * approach are welcome. + * + * @see hook_form_alter() + * + * @param $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param $form_id + */ + protected function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) { + $entity = $form_state->getFormObject()->getEntity(); + + if ($entity instanceof NodeType) { + $form['workflow']['options']['#default_value']['revision'] = 'revision'; + } + else if ($entity instanceof BlockContentType) { + $form['revision']['#default_value'] = 1; + $form['revision']['#disabled'] = TRUE; + $form['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.'); + } + } + +} diff --git a/src/Form/EntityModerationForm.php b/src/Form/EntityModerationForm.php new file mode 100644 index 0000000..7e148a9 --- /dev/null +++ b/src/Form/EntityModerationForm.php @@ -0,0 +1,147 @@ +getFormObject()->getEntity(); + $form['enable_moderation_state'] = [ + '#type' => 'checkbox', + '#title' => t('Enable moderation states.'), + '#description' => t('Content of this type must transition through moderation states in order to be published.'), + '#default_value' => $bundle->getThirdPartySetting('moderation_state', 'enabled', FALSE), + ]; + $states = \Drupal::entityTypeManager()->getStorage('moderation_state')->loadMultiple(); + $options = []; + /** @var ModerationState $state */ + foreach ($states as $key => $state) { + $options[$key] = $state->label() . ' ' . ($state->isPublishedState() ? t('(published)') : t('(non-published)')); + } + $form['allowed_moderation_states'] = [ + '#type' => 'checkboxes', + '#title' => t('Allowed moderation states.'), + '#description' => t('The allowed moderation states this content-type can be assigned. You must select at least one published and one non-published state.'), + '#default_value' => $bundle->getThirdPartySetting('moderation_state', 'allowed_moderation_states', []), + '#options' => $options, + '#states' => [ + 'visible' => [ + ':input[name=enable_moderation_state]' => ['checked' => TRUE], + ], + ], + ]; + $form['default_moderation_state'] = [ + '#type' => 'select', + '#title' => t('Default moderation state'), + '#empty_option' => t('-- Select --'), + '#options' => $options, + '#description' => t('Select the moderation state for new content'), + '#default_value' => $bundle->getThirdPartySetting('moderation_state', 'default_moderation_state', ''), + '#states' => [ + 'visible' => [ + ':input[name=enable_moderation_state]' => ['checked' => TRUE], + ], + ], + ]; + $form['#entity_builders'][] = [$this, 'formBuilderCallback']; + + return parent::form($form, $form_state); + } + + /** + * Form builder callback. + * + * @todo I don't know why this needs to be separate from the form() method. + * It was in the form_alter version but we should see if we can just fold + * it into the method above. + * + * @param $entity_type + * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function formBuilderCallback($entity_type, ConfigEntityInterface $bundle, &$form, FormStateInterface $form_state) { + // @todo write a test for this. + $bundle->setThirdPartySetting('moderation_state', 'enabled', $form_state->getValue('enable_moderation_state')); + $bundle->setThirdPartySetting('moderation_state', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states')))); + $bundle->setThirdPartySetting('moderation_state', 'default_moderation_state', $form_state->getValue('default_moderation_state')); + } + + /** + * @inheritDoc + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // @todo write a test for this. + if ($form_state->getValue('enable_moderation_state')) { + $states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple(); + $published = FALSE; + $non_published = TRUE; + $allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states'))); + foreach ($allowed as $state_id) { + /** @var ModerationState $state */ + $state = $states[$state_id]; + if ($state->isPublishedState()) { + $published = TRUE; + } + else { + $non_published = TRUE; + } + } + if (!$published || !$non_published) { + $form_state->setErrorByName('allowed_moderation_states', t('You must select at least one published moderation and one non-published state.')); + } + if (($default = $form_state->getValue('default_moderation_state')) && !empty($default)) { + if (!in_array($default, $allowed, TRUE)) { + $form_state->setErrorByName('default_moderation_state', t('The default moderation state must be one of the allowed states.')); + } + } + else { + $form_state->setErrorByName('default_moderation_state', t('You must select a default moderation state.')); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + // If moderation is enabled, revisions MUST be enabled as well. + // Otherwise we can't have forward revisions. + if($form_state->getValue('enable_moderation_state')) { + /* @var ConfigEntityTypeInterface $bundle */ + $bundle = $form_state->getFormObject()->getEntity(); + $bundle->setNewRevision(TRUE); + $bundle->save(); + } + + parent::submitForm( $form, $form_state); + + drupal_set_message($this->t('Your settings have been saved.')); + } +} diff --git a/src/LatestRevisionTrait.php b/src/LatestRevisionTrait.php new file mode 100644 index 0000000..46d8810 --- /dev/null +++ b/src/LatestRevisionTrait.php @@ -0,0 +1,53 @@ +entityTypeManager)) { + return $this->entityTypeManager; + } + if (isset($this->entityManager)) { + return $this->entityManager; + } + + return \Drupal::service('entity_type.manager'); + } + + /** + * Loads the latest revision of a specific entity. + * + * @param string $entity_type_id + * The entity type ID. + * @param int $entity_id + * The entity ID. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The latest entity revision or NULL, if the entity type / entity doesn't + * exist. + */ + protected function getLatestRevision($entity_type_id, $entity_id) { + if ($storage = $this->getEntityTypeManager()->getStorage($entity_type_id)) { + $revision_ids = $storage->getQuery() + ->allRevisions() + ->condition($this->getEntityTypeManager()->getDefinition($entity_type_id)->getKey('id'), $entity_id) + ->sort($this->getEntityTypeManager()->getDefinition($entity_type_id)->getKey('revision'), 'DESC') + ->pager(1) + ->execute(); + if ($revision_ids) { + $revision_id = array_keys($revision_ids)[0]; + return $storage->loadRevision($revision_id); + } + } + } + +} diff --git a/src/ModerationInformation.php b/src/ModerationInformation.php new file mode 100644 index 0000000..6efbf4f --- /dev/null +++ b/src/ModerationInformation.php @@ -0,0 +1,170 @@ +entityTypeManager = $entity_type_manager; + $this->currentUser = $current_user; + } + + /** + * Determines if an entity is one we should be moderating. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity we may be moderating. + * + * @return bool + * TRUE if this is an entity that we should act upon, FALSE otherwise. + */ + public function isModeratableEntity(EntityInterface $entity) { + if (!$entity instanceof ContentEntityInterface) { + return FALSE; + } + + return $this->isModeratableBundle($entity->getEntityType(), $entity->bundle()); + } + + /** + * Loads a specific bundle entity. + * + * @param string $bundle_entity_type_id + * The bundle entity type ID. + * @param string $bundle_id + * The bundle ID. + * + * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null + */ + protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) { + if ($bundle_entity_type_id) { + return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id); + } + } + + /** + * Determines if an entity type/bundle is one that will be moderated. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition to check. + * @param string $bundle + * The bundle to check. + * + * @return bool + * TRUE if this is a bundle we want to moderate, FALSE otherwise. + */ + public function isModeratableBundle(EntityTypeInterface $entity_type, $bundle) { + if ($bundle_entity = $this->loadBundleEntity($entity_type->getBundleEntityType(), $bundle)) { + return $bundle_entity->getThirdPartySetting('moderation_state', 'enabled', FALSE); + } + return FALSE; + } + + /** + * Filters an entity list to just bundle definitions for revisionable entities. + * + * @param EntityTypeInterface[] $entity_types + * The master entity type list filter. + * @return array + * An array of only the config entities we want to modify. + */ + public function selectRevisionableEntityTypes(array $entity_types) { + return array_filter($entity_types, function (EntityTypeInterface $type) use ($entity_types) { + return ($type instanceof ConfigEntityTypeInterface) + && $type->get('bundle_of') + && $entity_types[$type->get('bundle_of')]->isRevisionable(); + }); + } + + /** + * Determines if a config entity is a bundle for entities that may be moderated. + * + * This is the same check as exists in selectRevisionableEntityTypes(), but + * that one cannot use the entity manager due to recursion and this one + * doesn't have the entity list otherwise so must use the entity manager. The + * alternative would be to call getDefinitions() on entityTypeManager and use + * that in a sub-call, but that would be unnecessarily memory intensive. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check. + * @return bool + * TRUE if we want to add a Moderation operation to this entity, FALSE + * otherwise. + */ + public function isBundleForModeratableEntity(EntityInterface $entity) { + $type = $entity->getEntityType(); + + return + $type instanceof ConfigEntityTypeInterface + && $type->get('bundle_of') + && $this->entityTypeManager->getDefinition($type->get('bundle_of'))->isRevisionable() + && $this->currentUser->hasPermission('administer moderation state'); + } + + /** + * Determines if this form is for a moderated entity. + * + * @param \Drupal\Core\Form\FormInterface $form_object + * The form definition object for this form. + * @return bool + * TRUE if the form is for an entity that is subject to moderation, FALSe + * otherwise. + */ + public function isModeratedEntityForm(FormInterface $form_object) { + return $form_object instanceof ContentEntityFormInterface + && $this->isModeratableEntity($form_object->getEntity()); + } + + /** + * Determines if the form is the bundle edit of a revisionable entity. + * + * The logic here is not entirely clear, but seems to work. The form- and + * entity-dereference chaining seems excessive but is what works. + * + * @param \Drupal\Core\Form\FormInterface $form_object + * The form definition object for this form. + * @return bool + * True if the form is the bundle edit form for an entity type that supports + * revisions, false otherwise. + */ + public function isRevisionableBundleForm(FormInterface $form_object) { + // We really shouldn't be checking for a base class, but core lacks an + // interface here. When core adds a better way to determine if we're on + // a Bundle configuration form we should switch to that. + if ($form_object instanceof BundleEntityFormBase) { + $bundle_of = $form_object->getEntity()->getEntityType()->getBundleOf(); + $type = $this->entityTypeManager->getDefinition($bundle_of); + return $type->isRevisionable(); + } + + return FALSE; + } +} diff --git a/src/NodeEventSubscriber.php b/src/NodeEventSubscriber.php deleted file mode 100644 index bcd8d56..0000000 --- a/src/NodeEventSubscriber.php +++ /dev/null @@ -1,62 +0,0 @@ -entityTypeManager = $entity_type_manager; - } - - /** - * Acts on a node and set the published status based on the moderation state. - * - * @param \Drupal\node\NodeInterface $node - * The node being saved. - */ - public function nodePresave(NodeInterface $node) { - /* @var \Drupal\node\NodeTypeInterface $node_type */ - $node_type = $this->entityTypeManager->getStorage('node_type')->load($node->bundle()); - if (!$node_type->getThirdPartySetting('moderation_state', 'enabled', FALSE)) { - // @todo write a test for this. - return; - } - // @todo write a test for this. - if ($node->moderation_state->entity) { - $original = !empty($node->original) ? $node->original : NULL; - if ($original && $original->moderation_state->target_id !== $node->moderation_state->target_id) { - // We're moving to a new state, so force a new revision. - $node->setNewRevision(TRUE); - if ((!$original->moderation_state->entity && $original->isPublished()) || ($original->moderation_state->entity->isPublishedState() && !$node->moderation_state->entity->isPublishedState())) { - // Mark this as a new forward revision. - $node->isDefaultRevision(FALSE); - } - } - - $node->setPublished($node->moderation_state->entity->isPublishedState()); - } - } - -} diff --git a/src/ParamConverter/NodeRevisionConverter.php b/src/ParamConverter/EntityRevisionConverter.php similarity index 60% rename from src/ParamConverter/NodeRevisionConverter.php rename to src/ParamConverter/EntityRevisionConverter.php index 3a63d88..e92fac2 100644 --- a/src/ParamConverter/NodeRevisionConverter.php +++ b/src/ParamConverter/EntityRevisionConverter.php @@ -1,7 +1,7 @@ getDefault('_entity_form') === 'node.edit'; + if ($default = $route->getDefault('_entity_form') ) { + list($entity_type_id, $operation) = explode('.', $default); + if (!$this->entityManager->hasDefinition($entity_type_id)) { + return FALSE; + } + $entity_type = $this->entityManager->getDefinition($entity_type_id); + return $operation == 'edit' && $entity_type && $entity_type->isRevisionable(); + } } /** @@ -28,12 +38,7 @@ class NodeRevisionConverter extends EntityConverter { */ public function convert($value, $definition, $name, array $defaults) { $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); - if ($storage = $this->entityManager->getStorage($entity_type_id)) { - $entity = $storage->load($value); - $revision_ids = $storage->revisionIds($entity); - sort($revision_ids); - $latest = end($revision_ids); - $entity = $storage->loadRevision($latest); + if ($entity = $this->getLatestRevision($entity_type_id, $value)) { // If the entity type is translatable, ensure we return the proper // translation object for the current context. if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { diff --git a/src/Plugin/Derivative/DynamicLocalTasks.php b/src/Plugin/Derivative/DynamicLocalTasks.php new file mode 100644 index 0000000..c55ef2e --- /dev/null +++ b/src/Plugin/Derivative/DynamicLocalTasks.php @@ -0,0 +1,93 @@ +entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + $this->basePluginId = $base_plugin_id; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('entity_type.manager'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + + foreach ($this->moderatableEntityTypes() as $entity_type_id => $entity_type) { + $this->derivatives["$entity_type_id.moderation_tab"] = [ + 'route_name' => "entity.$entity_type_id.moderation", + 'title' => $this->t('Manage moderation'), + // @todo - are we sure they all have an edit_form? + 'base_route' => "entity.$entity_type_id.edit_form", + 'weight' => 30, + ] + $base_plugin_definition; + } + + return $this->derivatives; + } + + /** + * Returns an iterable of the entities to which to attach local tasks. + * + * @return array + * An array of just those entity types we care about. + */ + protected function moderatableEntityTypes() { + $entity_types = $this->entityTypeManager->getDefinitions(); + + return array_filter($entity_types, function (EntityTypeInterface $type) use ($entity_types) { + return ($type instanceof ConfigEntityTypeInterface) && $type->get('bundle_of') && $entity_types[$type->get('bundle_of')]->isRevisionable(); + }); + } +} diff --git a/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index 7ffb3b9..3c113e2 100644 --- a/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -7,7 +7,10 @@ namespace Drupal\moderation_state\Plugin\Field\FieldWidget; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; @@ -16,7 +19,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\moderation_state\Entity\ModerationState; -use Drupal\node\NodeInterface; +use Drupal\moderation_state\ModerationInformation; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,13 +43,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact protected $currentUser; /** - * Node type storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $nodeTypeStorage; - - /** * Moderation state transition entity query. * * @var \Drupal\Core\Entity\Query\QueryInterface @@ -59,23 +55,23 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $moderationStateStorage; + /** - * {@inheritdoc} + * @var \Drupal\moderation_state\ModerationInformation */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $plugin_id, - $plugin_definition, - $configuration['field_definition'], - $configuration['settings'], - $configuration['third_party_settings'], - $container->get('current_user'), - $container->get('entity_type.manager')->getStorage('node_type'), - $container->get('entity_type.manager')->getStorage('moderation_state'), - $container->get('entity_type.manager')->getStorage('moderation_state_transition'), - $container->get('entity.query')->get('moderation_state_transition', 'AND') - ); - } + protected $moderationInformation; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $moderationStateTransitionStorage; /** * Constructs a new ModerationStateWidget object. @@ -92,8 +88,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact * Third party settings. * @param \Drupal\Core\Session\AccountInterface $current_user * Current user service. - * @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage - * Node type storage. * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_storage * Moderation state storage. * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_transition_storage @@ -101,36 +95,56 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query * Moderation transation entity query service. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $node_type_storage, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query, ModerationInformation $moderation_information) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - $this->nodeTypeStorage = $node_type_storage; $this->moderationStateTransitionEntityQuery = $entity_query; $this->moderationStateTransitionStorage = $moderation_state_transition_storage; $this->moderationStateStorage = $moderation_state_storage; + $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; + $this->moderationInformation = $moderation_information; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('entity_type.manager')->getStorage('moderation_state'), + $container->get('entity_type.manager')->getStorage('moderation_state_transition'), + $container->get('entity.query')->get('moderation_state_transition', 'AND'), + $container->get('moderation_state.moderation_information') + ); } /** * {@inheritdoc} */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $node = $items->getEntity(); - /* @var \Drupal\node\NodeTypeInterface $node_type */ - $node_type = $this->nodeTypeStorage->load($node->bundle()); - if (!$node_type->getThirdPartySetting('moderation_state', 'enabled', FALSE)) { + $entity = $items->getEntity(); + /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle_entity */ + $bundle_entity = $this->entityTypeManager->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle()); + if (!$this->moderationInformation->isModeratableEntity($entity)) { // @todo write a test for this. return $element + ['#access' => FALSE]; } $options = $this->fieldDefinition ->getFieldStorageDefinition() - ->getOptionsProvider($this->column, $node) + ->getOptionsProvider($this->column, $entity) ->getSettableOptions($this->currentUser); - $default = $items->get($delta)->target_id ?: $node_type->getThirdPartySetting('moderation_state', 'default_moderation_state', FALSE); + $default = $items->get($delta)->target_id ?: $bundle_entity->getThirdPartySetting('moderation_state', 'default_moderation_state', FALSE); /** @var \Drupal\moderation_state\ModerationStateInterface $default_state */ $default_state = ModerationState::load($default); if (!$default || !$default_state) { - throw new \UnexpectedValueException(sprintf('The %s node type has an invalid moderation state configuration, moderation states are enabled but no default is set.', $node_type->label())); + throw new \UnexpectedValueException(sprintf('The %s bundle has an invalid moderation state configuration, moderation states are enabled but no default is set.', $bundle_entity->label())); } // @todo write a test for this. $from = $this->moderationStateTransitionEntityQuery @@ -139,7 +153,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact // Can always keep this one as is. $to[$default] = $default; // @todo write a test for this. - $allowed = $node_type->getThirdPartySetting('moderation_state', 'allowed_moderation_states', []); + $allowed = $bundle_entity->getThirdPartySetting('moderation_state', 'allowed_moderation_states', []); if ($from) { /* @var \Drupal\moderation_state\ModerationStateTransitionInterface $transition */ foreach ($this->moderationStateTransitionStorage->loadMultiple($from) as $id => $transition) { @@ -159,7 +173,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact '#default_value' => $default, '#published' => $default ? $default_state->isPublishedState() : FALSE, ]; - if ($this->currentUser->hasPermission('administer nodes') && count($options)) { + if ($this->currentUser->hasPermission($this->getAdminPermission($entity->getEntityType())) && count($options)) { // Use the dropbutton. $element['#process'][] = [$this, 'processActions']; // Don't show in sidebar/body. @@ -181,22 +195,31 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact return $element; } + protected function getAdminPermission(EntityTypeInterface $entity_type) { + switch ($entity_type->id()) { + case 'node': + return 'administer nodes'; + default: + return $entity_type->getAdminPermission(); + } + } + /** * Entity builder updating the node moderation state with the submitted value. * * @param string $entity_type_id * The entity type identifier. - * @param \Drupal\node\NodeInterface $node - * The node updated with the submitted values. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity updated with the submitted values. * @param array $form * The complete form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ - public function updateStatus($entity_type_id, NodeInterface $node, array $form, FormStateInterface $form_state) { + public function updateStatus($entity_type_id, ContentEntityInterface $entity, array $form, FormStateInterface $form_state) { $element = $form_state->getTriggeringElement(); if (isset($element['#moderation_state'])) { - $node->moderation_state->target_id = $element['#moderation_state']; + $entity->moderation_state->target_id = $element['#moderation_state']; } } /** @@ -251,6 +274,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact * {@inheritdoc} */ public static function isApplicable(FieldDefinitionInterface $field_definition) { - return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state' && $field_definition->getTargetEntityTypeId() === 'node'; + return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state'; } } diff --git a/src/Plugin/Menu/EditTab.php b/src/Plugin/Menu/EditTab.php index e488ed9..ba51477 100644 --- a/src/Plugin/Menu/EditTab.php +++ b/src/Plugin/Menu/EditTab.php @@ -7,11 +7,15 @@ namespace Drupal\moderation_state\Plugin\Menu; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Menu\LocalTaskDefault; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\moderation_state\LatestRevisionTrait; +use Drupal\moderation_state\ModerationInformation; use Drupal\node\NodeStorageInterface; use Drupal\node\NodeTypeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -22,40 +26,28 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterface { use StringTranslationTrait; + use LatestRevisionTrait; /** - * Node storage handler. + * The entity type manager. * - * @var \Drupal\node\NodeStorageInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $nodeStorage; + protected $entityTypeManager; /** - * Node type storage handler. + * The moderatio information service. * - * @var \Drupal\Core\Entity\EntityStorageInterface + * @var \Drupal\moderation_state\ModerationInformation */ - protected $nodeTypeStorage; + protected $moderationInformation; /** - * Node for route. + * The entity. * - * @var \Drupal\node\NodeInterface + * @var \Drupal\Core\Entity\ContentEntityInterface */ - protected $node; - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager')->getStorage('node'), - $container->get('entity_type.manager')->getStorage('node_type') - ); - } + protected $entity; /** * Constructs a new EditTab object. @@ -66,15 +58,29 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac * Plugin ID. * @param mixed $plugin_definition * Plugin definition. - * @param \Drupal\node\NodeStorageInterface $node_storage - * Node storage handler. - * @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage - * Node type storage handler + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\moderation_state\ModerationInformation $moderation_information + * The modertation information. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, NodeStorageInterface $node_storage, EntityStorageInterface $node_type_storage) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->nodeStorage = $node_storage; - $this->nodeTypeStorage = $node_type_storage; + + $this->entityTypeManager = $entity_type_manager; + $this->moderationInformation = $moderation_information; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('moderation_state.moderation_information') + ); } /** @@ -82,7 +88,7 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac */ public function getRouteParameters(RouteMatchInterface $route_match) { // Override the node here with the latest revision. - $this->node = $route_match->getParameter('node'); + $this->entity = $route_match->getParameter($this->pluginDefinition['entity_type_id']); return parent::getRouteParameters($route_match); } @@ -90,17 +96,15 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac * {@inheritdoc} */ public function getTitle() { - /* @var NodeTypeInterface $node_type */ - // @todo write a test for this. - $node_type = $this->nodeTypeStorage->load($this->node->bundle()); - if (!$node_type->getThirdPartySetting('moderation_state', 'enabled', FALSE)) { + if (!$this->moderationInformation->isModeratableEntity($this->entity)) { // Moderation isn't enabled. return parent::getTitle(); } - $revision_ids = $this->nodeStorage->revisionIds($this->node); - sort($revision_ids); - $latest = end($revision_ids); - if ($this->node->getRevisionId() === $latest && $this->node->isDefaultRevision() && $this->node->moderation_state->entity && $this->node->moderation_state->entity->isPublishedState()) { + + // @todo write a test for this. + /** @var ContentEntityInterface $latest */ + $latest = $this->getLatestRevision($this->entity->getEntityTypeId(), $this->entity->id()); + if ($this->entity->getRevisionId() === $latest->getRevisionId() && $this->entity->isDefaultRevision() && $this->entity->moderation_state->entity && $this->entity->moderation_state->entity->isPublishedState()) { // @todo write a test for this. return $this->t('New draft'); } @@ -117,8 +121,8 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac // @todo write a test for this. $tags = parent::getCacheTags(); // Tab changes if node or node-type is modified. - $tags[] = 'node:' . $this->node->id(); - $tags[] = 'node_type:' . $this->node->bundle(); + $tags = array_merge($tags, $this->entity->getCacheTags()); + $tags[] = $this->entity->getEntityType()->getBundleEntityType() . ':' . $this->entity->bundle(); return $tags; } diff --git a/src/Routing/ModerationRouteProvider.php b/src/Routing/ModerationRouteProvider.php new file mode 100644 index 0000000..ffa40e4 --- /dev/null +++ b/src/Routing/ModerationRouteProvider.php @@ -0,0 +1,65 @@ +getModerationFormRoute($entity_type)) { + $entity_type_id = $entity_type->id(); + $collection->add("entity.{$entity_type_id}.moderation", $moderation_route); + } + + return $collection; + } + + /** + * Gets the moderation-form route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getModerationFormRoute(EntityTypeInterface $entity_type) { + if ($entity_type->hasLinkTemplate('moderation-form') && $entity_type->getFormClass('moderation')) { + $entity_type_id = $entity_type->id(); + + $route = new Route($entity_type->getLinkTemplate('moderation-form')); + + $route + ->setDefaults([ + '_entity_form' => "{$entity_type_id}.moderation", + '_title' => 'Moderation', + //'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::editTitle' + ]) + ->setRequirement('_permission', 'administer moderation state') // @todo Come up with a new permission. + ->setOption('parameters', [ + $entity_type_id => ['type' => 'entity:' . $entity_type_id], + ]); + + return $route; + } + } +} diff --git a/src/Tests/ModerationStateNodeTypeTest.php b/src/Tests/ModerationStateNodeTypeTest.php index bb34971..3b0f2a9 100644 --- a/src/Tests/ModerationStateNodeTypeTest.php +++ b/src/Tests/ModerationStateNodeTypeTest.php @@ -46,14 +46,14 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase { ], t('Save and publish')); $this->assertText('Not moderated Test has been created.'); // Now enable moderation state. - $this->drupalGet('admin/structure/types/manage/not_moderated'); + $this->drupalGet('admin/structure/types/manage/not_moderated/moderation'); $this->drupalPostForm(NULL, [ 'enable_moderation_state' => 1, 'allowed_moderation_states[draft]' => 1, 'allowed_moderation_states[needs_review]' => 1, 'allowed_moderation_states[published]' => 1, 'default_moderation_state' => 'draft', - ], t('Save content type')); + ], t('Save')); $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ 'title' => 'Test' ]); @@ -88,20 +88,24 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase { protected function createContentTypeFromUI($content_type_name, $content_type_id, $moderated = FALSE, $allowed_states = [], $default_state = NULL) { $this->drupalGet('admin/structure/types'); $this->clickLink('Add content type'); - $this->assertFieldByName('enable_moderation_state'); - $this->assertNoFieldChecked('edit-enable-moderation-state'); $edit = [ 'name' => $content_type_name, 'type' => $content_type_id, ]; + $this->drupalPostForm(NULL, $edit, t('Save content type')); + if ($moderated) { + $this->drupalGet('admin/structure/types/' . $content_type_id . '/moderation'); + $this->assertFieldByName('enable_moderation_state'); + $this->assertNoFieldChecked('edit-enable-moderation-state'); $edit['enable_moderation_state'] = 1; foreach ($allowed_states as $state) { $edit['allowed_moderation_states[' . $state . ']'] = 1; } $edit['default_moderation_state'] = $default_state; + + $this->drupalPostForm('admin/structure/types/' . $content_type_id . '/moderation', $edit, t('Save')); } - $this->drupalPostForm(NULL, $edit, t('Save content type')); } /** diff --git a/tests/src/Kernel/EntityOperationsTest.php b/tests/src/Kernel/EntityOperationsTest.php new file mode 100644 index 0000000..19d2ab8 --- /dev/null +++ b/tests/src/Kernel/EntityOperationsTest.php @@ -0,0 +1,153 @@ +installEntitySchema('node'); + $this->installSchema('node', 'node_access'); + $this->installEntitySchema('user'); + $this->installConfig('moderation_state'); + + $this->createNodeType(); + } + + /** + * Creates a page node type to test with, ensuring that it's moderatable. + */ + protected function createNodeType() { + $node_type = NodeType::create([ + 'type' => 'page', + 'label' => 'Page', + ]); + $node_type->setThirdPartySetting('moderation_state', 'enabled', TRUE); + $node_type->save(); + } + + /** + * Verifies that the process of saving forward-revisions works as expected. + */ + public function testForwardRevisions() { + // Create a new node in draft. + $page = Node::create([ + 'type' => 'page', + 'title' => 'A', + ]); + $page->moderation_state->target_id = 'draft'; + $page->save(); + + $id = $page->id(); + + // Verify the entity saved correctly. + /** @var Node $page */ + $page = Node::load($id); + $this->assertEquals('A', $page->getTitle()); + $this->assertTrue($page->isDefaultRevision()); + $this->assertFalse($page->isPublished()); + + // Moderate the entity to published. + $page->setTitle('B'); + $page->moderation_state->target_id = 'published'; + $page->save(); + + // Verify the entity is now published and public. + $page = Node::load($id); + $this->assertEquals('B', $page->getTitle()); + $this->assertTrue($page->isDefaultRevision()); + $this->assertTrue($page->isPublished()); + + // Make a new forward-revision in Draft. + $page->setTitle('C'); + $page->moderation_state->target_id = 'draft'; + $page->save(); + + // Verify normal loads return the still-default previous version. + $page = Node::load($id); + $this->assertEquals('B', $page->getTitle()); + + // Verify we can load the forward revision, even if the mechanism is kind + // of gross. Note: revisionIds() is only available on NodeStorageInterface, + // so this won't work for non-nodes. We'd need to use entity queries. This + // is a core bug that should get fixed. + $storage = \Drupal::entityTypeManager()->getStorage('node'); + $revision_ids = $storage->revisionIds($page); + sort($revision_ids); + $latest = end($revision_ids); + $page = $storage->loadRevision($latest); + $this->assertEquals('C', $page->getTitle()); + + $page->setTitle('D'); + $page->moderation_state->target_id = 'published'; + $page->save(); + + // Verify normal loads return the still-default previous version. + $page = Node::load($id); + $this->assertEquals('D', $page->getTitle()); + $this->assertTrue($page->isDefaultRevision()); + $this->assertTrue($page->isPublished()); + + // Now check that we can immediately add a new published revision over it. + $page->setTitle('E'); + $page->moderation_state->target_id = 'published'; + $page->save(); + + $page = Node::load($id); + $this->assertEquals('E', $page->getTitle()); + $this->assertTrue($page->isDefaultRevision()); + $this->assertTrue($page->isPublished()); + } + + /** + * Verifies that a newly-created node can go straight to published. + */ + public function testPublishedCreation() { + // Create a new node in draft. + $page = Node::create([ + 'type' => 'page', + 'title' => 'A', + ]); + $page->moderation_state->target_id = 'published'; + $page->save(); + + $id = $page->id(); + + // Verify the entity saved correctly. + /** @var Node $page */ + $page = Node::load($id); + $this->assertEquals('A', $page->getTitle()); + $this->assertTrue($page->isDefaultRevision()); + $this->assertTrue($page->isPublished()); + + } + +} diff --git a/tests/src/Kernel/EntityRevisionConverterTest.php b/tests/src/Kernel/EntityRevisionConverterTest.php new file mode 100644 index 0000000..4e06f31 --- /dev/null +++ b/tests/src/Kernel/EntityRevisionConverterTest.php @@ -0,0 +1,89 @@ +installEntitySchema('entity_test'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'router'); + $this->installSchema('system', 'sequences'); + $this->installSchema('node', 'node_access'); + \Drupal::service('router.builder')->rebuild(); + } + + public function testConvertNonRevisionableEntityType() { + $entity_test = EntityTest::create([ + 'name' => 'test', + ]); + + $entity_test->save(); + + /** @var \Symfony\Component\Routing\RouterInterface $router */ + $router = \Drupal::service('router.no_access_checks'); + $result = $router->match('/entity_test/' . $entity_test->id()); + + $this->assertInstanceOf(EntityTest::class, $result['entity_test']); + $this->assertEquals($entity_test->getRevisionId(), $result['entity_test']->getRevisionId()); + } + + public function testConvertWithRevisionableEntityType() { + $node_type = NodeType::create([ + 'type' => 'article', + ]); + $node_type->setThirdPartySetting('moderation_state', 'enabled', TRUE); + $node_type->save(); + + $revision_ids = []; + $node = Node::create([ + 'title' => 'test', + 'type' => 'article' + ]); + $node->save(); + + $revision_ids[] = $node->getRevisionId(); + + $node->setNewRevision(TRUE); + $node->save(); + $revision_ids[] = $node->getRevisionId(); + + $node->setNewRevision(TRUE); + $node->isDefaultRevision(FALSE); + $node->save(); + $revision_ids[] = $node->getRevisionId(); + + /** @var \Symfony\Component\Routing\RouterInterface $router */ + $router = \Drupal::service('router.no_access_checks'); + $result = $router->match('/node/' . $node->id() . '/edit'); + + $this->assertInstanceOf(Node::class, $result['node']); + $this->assertEquals($revision_ids[2], $result['node']->getRevisionId()); + $this->assertFalse($result['node']->isDefaultRevision()); + } + +} diff --git a/tests/src/Unit/ModerationInformationTest.php b/tests/src/Unit/ModerationInformationTest.php new file mode 100644 index 0000000..60a9276 --- /dev/null +++ b/tests/src/Unit/ModerationInformationTest.php @@ -0,0 +1,133 @@ +prophesize(AccountInterface::class)->reveal(); + } + + protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_storage) { + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('entity_test_bundle')->willReturn($entity_bundle_storage); + return $entity_type_manager->reveal(); + } + + public function setupModerationEntityManager($status) { + $bundle = $this->prophesize(ConfigEntityInterface::class); + $bundle->getThirdPartySetting('moderation_state', 'enabled', FALSE)->willReturn($status); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load('test_bundle')->willReturn($bundle->reveal()); + + return $this->getEntityTypeManager($entity_storage->reveal()); + } + + /** + * @dataProvider providerBoolean + * @covers ::isModeratableEntity + */ + public function testIsModeratableEntity($status) { + $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser()); + + $entity_type = new ContentEntityType([ + 'id' => 'test_entity_type', + 'bundle_entity_type' => 'entity_test_bundle', + ]); + $entity = $this->prophesize(ContentEntityInterface::class); + $entity->getEntityType()->willReturn($entity_type); + $entity->bundle()->willReturn('test_bundle'); + + $this->assertEquals($status, $moderation_information->isModeratableEntity($entity->reveal())); + } + + /** + * @covers ::isModeratableEntity + */ + public function testIsModeratableEntityForNonBundleEntityType() { + $entity_type = new ContentEntityType([ + 'id' => 'test_entity_type', + ]); + $entity = $this->prophesize(ContentEntityInterface::class); + $entity->getEntityType()->willReturn($entity_type); + $entity->bundle()->willReturn('test_entity_type'); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_type_manager = $this->getEntityTypeManager($entity_storage->reveal()); + $moderation_information = new ModerationInformation($entity_type_manager, $this->getUser()); + + $this->assertEquals(FALSE, $moderation_information->isModeratableEntity($entity->reveal())); + } + + /** + * @dataProvider providerBoolean + * @covers ::isModeratableBundle + */ + public function testIsModeratableBundle($status) { + $entity_type = new ContentEntityType([ + 'id' => 'test_entity_type', + 'bundle_entity_type' => 'entity_test_bundle', + ]); + + $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser()); + + $this->assertEquals($status, $moderation_information->isModeratableBundle($entity_type, 'test_bundle')); + } + + /** + * @dataProvider providerBoolean + * @covers ::isModeratedEntityForm + */ + public function testIsModeratedEntityForm($status) { + $entity_type = new ContentEntityType([ + 'id' => 'test_entity_type', + 'bundle_entity_type' => 'entity_test_bundle', + ]); + + $entity = $this->prophesize(ContentEntityInterface::class); + $entity->getEntityType()->willReturn($entity_type); + $entity->bundle()->willReturn('test_bundle'); + + $form = $this->prophesize(ContentEntityFormInterface::class); + $form->getEntity()->willReturn($entity); + + $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser()); + + $this->assertEquals($status, $moderation_information->isModeratedEntityForm($form->reveal())); + } + + public function testIsModeratedEntityFormWithNonContentEntityForm() { + $form = $this->prophesize(EntityFormInterface::class); + $moderation_information = new ModerationInformation($this->setupModerationEntityManager(TRUE), $this->getUser()); + + $this->assertFalse($moderation_information->isModeratedEntityForm($form->reveal())); + } + + public function providerBoolean() { + return [ + [FALSE], + [TRUE], + ]; + } + +}