Core and composer updates

This commit is contained in:
Rob Davies 2017-07-03 16:47:07 +01:00
parent a82634bb98
commit 62cac30480
1118 changed files with 21770 additions and 6306 deletions

View file

@ -1,5 +1,7 @@
entity-moderation-form:
content_moderation:
version: VERSION
css:
layout:
css/entity-moderation-form.css: {}
component:
css/content_moderation.module.css: {}
theme:
css/content_moderation.theme.css: {}

View file

@ -10,7 +10,6 @@ use Drupal\content_moderation\EntityTypeInfo;
use Drupal\content_moderation\ContentPreprocess;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublishNode;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublishNode;
use Drupal\content_moderation\Plugin\Menu\EditTab;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
@ -99,22 +98,6 @@ function content_moderation_entity_update(EntityInterface $entity) {
->entityUpdate($entity);
}
/**
* Implements hook_local_tasks_alter().
*/
function content_moderation_local_tasks_alter(&$local_tasks) {
$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().
*/

View file

@ -2,7 +2,7 @@ view any unpublished content:
title: 'View any unpublished content'
description: 'This permission is necessary for any users that may moderate content.'
'view content moderation':
view content moderation:
title: 'View content moderation'
description: 'View content moderation.'

View file

@ -1,14 +1,17 @@
/**
* @file
* Component styles for the content_moderation module.
*/
ul.entity-moderation-form {
list-style: none;
display: -webkit-flex; /* Safari */
display: flex;
-webkit-flex-wrap: wrap; /* Safari */
flex-wrap: wrap;
flex-wrap: wrap;
-webkit-justify-content: space-around; /* Safari */
justify-content: space-around;
justify-content: space-around;
-webkit-align-items: flex-end; /* Safari */
align-items: flex-end;
border-bottom: 1px solid gray;
align-items: flex-end;
}
ul.entity-moderation-form input[type=submit] {

View file

@ -0,0 +1,7 @@
/**
* @file
* Theme styles for the content_moderation module.
*/
ul.entity-moderation-form {
border-bottom: 1px solid gray;
}

View file

@ -7,6 +7,8 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\Routing\Route;
/**
@ -41,18 +43,31 @@ class LatestRevisionCheck implements AccessInterface {
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @see \Drupal\Core\Entity\EntityAccessCheck
*/
public function access(Route $route, RouteMatchInterface $route_match) {
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
// This tab should not show up unless there's a reason to show it.
$entity = $this->loadEntity($route, $route_match);
return $this->moderationInfo->hasForwardRevision($entity)
? AccessResult::allowed()->addCacheableDependency($entity)
: AccessResult::forbidden()->addCacheableDependency($entity);
if ($this->moderationInfo->hasForwardRevision($entity)) {
// Check the global permissions first.
$access_result = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view any unpublished content']);
if (!$access_result->isAllowed()) {
// Check entity owner access.
$owner_access = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view own unpublished content']);
$owner_access = $owner_access->andIf((AccessResult::allowedIf($entity instanceof EntityOwnerInterface && ($entity->getOwnerId() == $account->id()))));
$access_result = $access_result->orIf($owner_access);
}
return $access_result->addCacheableDependency($entity);
}
return AccessResult::forbidden()->addCacheableDependency($entity);
}
/**

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\content_moderation;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
/**
* The access control handler for the content_moderation_state entity type.
*
* @see \Drupal\content_moderation\Entity\ContentModerationState
*/
class ContentModerationStateAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
// ContentModerationState is an internal entity type. Access is denied for
// viewing, updating, and deleting. In order to update an entity's
// moderation state use its moderation_state field.
return AccessResult::forbidden('ContentModerationState is an internal entity type.');
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
// ContentModerationState is an internal entity type. Access is denied for
// creating. In order to update an entity's moderation state use its
// moderation_state field.
return AccessResult::forbidden('ContentModerationState is an internal entity type.');
}
}

View file

@ -16,11 +16,20 @@ class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
// Creates an index to ensure that the lookup in
// \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList::getModerationState()
// is performant.
$schema['content_moderation_state_field_data']['indexes'] += [
'content_moderation_state__lookup' => ['content_entity_type_id', 'content_entity_id', 'content_entity_revision_id'],
// Creates unique keys to guarantee the integrity of the entity and to make
// the lookup in ModerationStateFieldItemList::getModerationState() fast.
$unique_keys = [
'content_entity_type_id',
'content_entity_id',
'content_entity_revision_id',
'workflow',
'langcode',
];
$schema['content_moderation_state_field_data']['unique keys'] += [
'content_moderation_state__lookup' => $unique_keys,
];
$schema['content_moderation_state_field_revision']['unique keys'] += [
'content_moderation_state__lookup' => $unique_keys,
];
return $schema;

View file

@ -24,6 +24,7 @@ use Drupal\user\UserInterface;
* handlers = {
* "storage_schema" = "Drupal\content_moderation\ContentModerationStateStorageSchema",
* "views_data" = "\Drupal\views\EntityViewsData",
* "access" = "Drupal\content_moderation\ContentModerationStateAccessControlHandler",
* },
* base_table = "content_moderation_state",
* revision_table = "content_moderation_state_revision",
@ -74,6 +75,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
->setLabel(t('Content entity type ID'))
->setDescription(t('The ID of the content entity type this moderation state is for.'))
->setRequired(TRUE)
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH)
->setRevisionable(TRUE);
$fields['content_entity_id'] = BaseFieldDefinition::create('integer')
@ -82,10 +84,6 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
->setRequired(TRUE)
->setRevisionable(TRUE);
// @todo https://www.drupal.org/node/2779931 Add constraint that enforces
// unique content_entity_type_id, content_entity_id and
// content_entity_revision_id.
$fields['content_entity_revision_id'] = BaseFieldDefinition::create('integer')
->setLabel(t('Content entity revision ID'))
->setDescription(t('The revision ID of the content entity this moderation state is for.'))

View file

@ -105,9 +105,10 @@ class EntityOperations implements ContainerInjectionInterface {
/** @var \Drupal\content_moderation\ContentModerationState $current_state */
$current_state = $workflow->getState($entity->moderation_state->value);
// This entity is default if it is new, the default revision, or the
// default revision is not published.
// This entity is default if it is new, a new translation, the default
// revision, or the default revision is not published.
$update_default_revision = $entity->isNew()
|| $entity->isNewTranslation()
|| $current_state->isDefaultRevisionState()
|| !$this->isDefaultRevisionPublished($entity, $workflow);
@ -157,7 +158,7 @@ class EntityOperations implements ContainerInjectionInterface {
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if (!$moderation_state) {
$moderation_state = $workflow->getInitialState()->id();
$moderation_state = $workflow->getTypePlugin()->getInitialState($workflow, $entity)->id();
}
// @todo what if $entity->moderation_state is null at this point?
@ -181,8 +182,9 @@ class EntityOperations implements ContainerInjectionInterface {
]);
$content_moderation_state->workflow->target_id = $workflow->id();
}
else {
// Create a new revision.
elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
// If a new revision of the content has been created, add a new content
// moderation state revision.
$content_moderation_state->setNewRevision(TRUE);
}
@ -234,8 +236,7 @@ class EntityOperations implements ContainerInjectionInterface {
if (!$this->moderationInfo->isLatestRevision($entity)) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($entity->isDefaultRevision()) {
if ($this->moderationInfo->isLiveRevision($entity)) {
return;
}
@ -250,8 +251,8 @@ class EntityOperations implements ContainerInjectionInterface {
* Check if the default revision for the given entity is published.
*
* The default revision is the same as the entity retrieved by "default" from
* the storage handler. If the entity is translated, use the default revision
* of the same language as the given entity.
* the storage handler. If the entity is translated, check if any of the
* translations are published.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being saved.
@ -262,21 +263,22 @@ class EntityOperations implements ContainerInjectionInterface {
* TRUE if the default revision is published. FALSE otherwise.
*/
protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$default_revision = $storage->load($entity->id());
$default_revision = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id());
// Ensure we are comparing the same translation as the current entity.
// Ensure we are checking all translations of the default revision.
if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
// If there is no translation, then there is no default revision and is
// therefore not published.
if (!$default_revision->hasTranslation($entity->language()->getId())) {
return FALSE;
// Loop through each language that has a translation.
foreach ($default_revision->getTranslationLanguages() as $language) {
// Load the translated revision.
$language_revision = $default_revision->getTranslation($language->getId());
// Return TRUE if a translation with a published state is found.
if ($workflow->getState($language_revision->moderation_state->value)->isPublishedState()) {
return TRUE;
}
}
$default_revision = $default_revision->getTranslation($entity->language()->getId());
}
return $default_revision && $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
return $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
}
}

View file

@ -3,6 +3,7 @@
namespace Drupal\content_moderation\Form;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\content_moderation\ModerationInformationInterface;
@ -127,8 +128,11 @@ class EntityModerationForm extends FormBase {
$new_state = $form_state->getValue('new_state');
$entity->set('moderation_state', $new_state);
$entity->revision_log = $form_state->getValue('revision_log');
if ($entity instanceof RevisionLogInterface) {
$entity->setRevisionLogMessage($form_state->getValue('revision_log'));
$entity->setRevisionUserId($this->currentUser()->id());
}
$entity->save();
drupal_set_message($this->t('The moderation state has been updated.'));

View file

@ -98,7 +98,7 @@ class EntityRevisionConverter extends EntityConverter {
$latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, ['operation' => 'entity_upcast']);
}
if ($latest_revision->isRevisionTranslationAffected()) {
if ($latest_revision instanceof EntityInterface && $latest_revision->isRevisionTranslationAffected()) {
$entity = $latest_revision;
}
}

View file

@ -109,18 +109,13 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
/** @var ContentEntityInterface $entity */
$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->isModeratedEntity($entity)) {
// @todo https://www.drupal.org/node/2779933 write a test for this.
return $element + ['#access' => FALSE];
}
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
$default = $items->get($delta)->value ? $workflow->getState($items->get($delta)->value) : $workflow->getInitialState();
if (!$default) {
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()));
}
$default = $items->get($delta)->value ? $workflow->getState($items->get($delta)->value) : $workflow->getTypePlugin()->getInitialState($workflow, $entity);
/** @var \Drupal\workflows\Transition[] $transitions */
$transitions = $this->validator->getValidTransitions($entity, $this->currentUser);

View file

@ -37,8 +37,8 @@ class ModerationStateFieldItemList extends FieldItemList {
// It is possible that the bundle does not exist at this point. For example,
// the node type form creates a fake Node entity to get default values.
// @see \Drupal\node\NodeTypeForm::form()
$workflow = $moderation_info->getWorkflowForEntity($entity);
return $workflow ? $workflow->getInitialState()->id() : NULL;
$workflow = $moderation_info->getWorkFlowForEntity($entity);
return $workflow ? $workflow->getTypePlugin()->getInitialState($workflow, $entity)->id() : NULL;
}
/**

View file

@ -1,105 +0,0 @@
<?php
namespace Drupal\content_moderation\Plugin\Menu;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\content_moderation\ModerationInformation;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class for making the edit tab use 'Edit draft' or 'New draft'.
*/
class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformation
*/
protected $moderationInfo;
/**
* The entity if determinable from the route or FALSE.
*
* @var \Drupal\Core\Entity\ContentEntityInterface|FALSE
*/
protected $entity;
/**
* Constructs a new EditTab object.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* Plugin ID.
* @param mixed $plugin_definition
* Plugin definition.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation service.
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
* The moderation information.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $string_translation, ModerationInformation $moderation_information) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->stringTranslation = $string_translation;
$this->moderationInfo = $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('string_translation'),
$container->get('content_moderation.moderation_information')
);
}
/**
* {@inheritdoc}
*/
public function getRouteParameters(RouteMatchInterface $route_match) {
$entity_parameter = $route_match->getParameter($this->pluginDefinition['entity_type_id']);
$this->entity = $entity_parameter instanceof ContentEntityInterface ? $route_match->getParameter($this->pluginDefinition['entity_type_id']) : FALSE;
return parent::getRouteParameters($route_match);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// If the entity couldn't be loaded or moderation isn't enabled.
if (!$this->entity || !$this->moderationInfo->isModeratedEntity($this->entity)) {
return parent::getTitle();
}
return $this->moderationInfo->isLiveRevision($this->entity)
? $this->t('New draft')
: $this->t('Edit draft');
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = parent::getCacheTags();
// Tab changes if node or node-type is modified.
if ($this->entity) {
$tags = array_merge($tags, $this->entity->getCacheTags());
$tags[] = $this->entity->getEntityType()->getBundleEntityType() . ':' . $this->entity->bundle();
}
return $tags;
}
}

View file

@ -117,7 +117,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
protected function isFirstTimeModeration(EntityInterface $entity) {
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
$original_id = $original_entity->moderation_state;
if ($original_entity) {
$original_id = $original_entity->moderation_state;
}
return !($entity->moderation_state && $original_entity && $original_id);
}

View file

@ -4,6 +4,7 @@ namespace Drupal\content_moderation\Plugin\WorkflowType;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
@ -289,4 +290,14 @@ class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPlug
return $configuration;
}
/**
* {@inheritdoc}
*/
public function getInitialState(WorkflowInterface $workflow, $entity = NULL) {
if ($entity instanceof EntityPublishedInterface) {
return $workflow->getState($entity->isPublished() ? 'published' : 'draft');
}
return parent::getInitialState($workflow);
}
}

View file

@ -81,7 +81,6 @@ class EntityModerationRouteProvider implements EntityRouteProviderInterface, Ent
// If the entity type is a node, unpublished content will be visible
// if the user has the "view all unpublished content" permission.
->setRequirement('_entity_access', "{$entity_type_id}.view")
->setRequirement('_permission', 'view latest version,view any unpublished content')
->setRequirement('_content_moderation_latest_version', 'TRUE')
->setOption('_content_moderation_entity_type', $entity_type_id)
->setOption('parameters', [

View file

@ -40,7 +40,7 @@ class StateTransitionValidation implements StateTransitionValidationInterface {
*/
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
$current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getInitialState();
$current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getTypePlugin()->getInitialState($workflow, $entity);
return array_filter($current_state->getTransitions(), function(Transition $transition) use ($workflow, $user) {
return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());

View file

@ -1,106 +0,0 @@
<?php
namespace Drupal\content_moderation\Tests;
/**
* Tests the moderation form, specifically on nodes.
*
* @group content_moderation
*/
class ModerationFormTest extends ModerationStateTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
/**
* Tests the moderation form that shows on the latest version page.
*
* The latest version page only shows if there is a forward revision. There
* is only a forward revision if a draft revision is created on a node where
* the default revision is not a published moderation state.
*
* @see \Drupal\content_moderation\EntityOperations
* @see \Drupal\content_moderation\Tests\ModerationStateBlockTest::testCustomBlockModeration
*/
public function testModerationForm() {
// Create new moderated content in draft.
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'Some moderated content',
'body[0][value]' => 'First version of the content.',
], t('Save and Create New Draft'));
$node = $this->drupalGetNodeByTitle('Some moderated content');
$canonical_path = sprintf('node/%d', $node->id());
$edit_path = sprintf('node/%d/edit', $node->id());
$latest_version_path = sprintf('node/%d/latest', $node->id());
$this->assertTrue($this->adminUser->hasPermission('edit any moderated_content content'));
// The latest version page should not show, because there is no forward
// revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Update the draft.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Second version of the content.',
], t('Save and Create New Draft'));
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Publish the draft.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Third version of the content.',
], t('Save and Publish'));
// The published view should not have a moderation form, because it is the
// default revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertNoText('Status', 'The node view page has no moderation form.');
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Make a forward revision.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Fourth version of the content.',
], t('Save and Create New Draft'));
// The published view should not have a moderation form, because it is the
// default revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertNoText('Status', 'The node view page has no moderation form.');
// The latest version page should show the moderation form and have "Draft"
// status, because the forward revision is in "Draft".
$this->drupalGet($latest_version_path);
$this->assertResponse(200);
$this->assertText('Status', 'Form text found on the latest-version page.');
$this->assertText('Draft', 'Correct status found on the latest-version page.');
// Submit the moderation form to change status to published.
$this->drupalPostForm($latest_version_path, [
'new_state' => 'published',
], t('Apply'));
// The latest version page should not show, because there is no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
}
}

View file

@ -1,146 +0,0 @@
<?php
namespace Drupal\content_moderation\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\user\Entity\Role;
/**
* Defines a base class for moderation state tests.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\content_moderation\Functional\ModerationStateTestBase instead.
*/
abstract class ModerationStateTestBase extends WebTestBase {
/**
* Profile to use.
*/
protected $profile = 'testing';
/**
* Admin user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* Permissions to grant admin user.
*
* @var array
*/
protected $permissions = [
'administer content moderation',
'access administration pages',
'administer content types',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
'use editorial transition create_new_draft',
'use editorial transition publish',
];
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'content_moderation',
'block',
'block_content',
'node',
];
/**
* Sets the test up.
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser($this->permissions);
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
$this->drupalPlaceBlock('page_title_block');
$this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']);
}
/**
* Gets the permission machine name for a transition.
*
* @param string $workflow_id
* The workflow ID.
* @param string $transition_id
* The transition ID.
*
* @return string
* The permission machine name for a transition.
*/
protected function getWorkflowTransitionPermission($workflow_id, $transition_id) {
return 'use ' . $workflow_id . ' transition ' . $transition_id;
}
/**
* Creates a content-type from the UI.
*
* @param string $content_type_name
* Content type human name.
* @param string $content_type_id
* Machine name.
* @param bool $moderated
* TRUE if should be moderated.
* @param string $workflow_id
* The workflow to attach to the bundle.
*/
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'editorial') {
$this->drupalGet('admin/structure/types');
$this->clickLink('Add content type');
$edit = [
'name' => $content_type_name,
'type' => $content_type_id,
];
$this->drupalPostForm(NULL, $edit, t('Save content type'));
if ($moderated) {
$this->enableModerationThroughUi($content_type_id, $workflow_id);
}
}
/**
* Enable moderation for a specified content type, using the UI.
*
* @param string $content_type_id
* Machine name.
* @param string $workflow_id
* The workflow to attach to the bundle.
*/
protected function enableModerationThroughUi($content_type_id, $workflow_id = 'editorial') {
$edit['workflow'] = $workflow_id;
$this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save'));
// Ensure the parent environment is up-to-date.
// @see content_moderation_workflow_insert()
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
* Grants given user permission to create content of given type.
*
* @param \Drupal\Core\Session\AccountInterface $account
* User to grant permission to.
* @param string $content_type_id
* Content type ID.
*/
protected function grantUserPermissionToCreateContentOfType(AccountInterface $account, $content_type_id) {
$role_ids = $account->getRoles(TRUE);
/* @var \Drupal\user\RoleInterface $role */
$role_id = reset($role_ids);
$role = Role::load($role_id);
$role->grantPermission(sprintf('create %s content', $content_type_id));
$role->grantPermission(sprintf('edit any %s content', $content_type_id));
$role->grantPermission(sprintf('delete any %s content', $content_type_id));
$role->save();
}
}

View file

@ -1,4 +1,4 @@
{{ attach_library('content_moderation/entity-moderation-form') }}
{{ attach_library('content_moderation/content_moderation') }}
<ul class="entity-moderation-form">
<li>{{ form.current }}</li>
<li>{{ form.new_state }}</li>

View file

@ -1,96 +0,0 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\simpletest\ContentTypeCreationTrait;
use Drupal\simpletest\NodeCreationTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Test the content moderation local task.
*
* @group content_moderation
*/
class LocalTaskTest extends BrowserTestBase {
use ContentTypeCreationTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'content_moderation_test_local_task',
'content_moderation',
'block',
];
/**
* A test node.
*
* @var \Drupal\node\NodeInterface
*/
protected $testNode;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
$this->drupalLogin($this->createUser(['bypass node access']));
$node_type = $this->createContentType([
'type' => 'test_content_type',
]);
// Now enable moderation for subsequent nodes.
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', $node_type->id());
$workflow->save();
$this->testNode = $this->createNode([
'type' => $node_type->id(),
'moderation_state' => 'draft',
]);
}
/**
* Tests local tasks behave with content_moderation enabled.
*/
public function testLocalTasks() {
// The default state is a draft.
$this->drupalGet(sprintf('node/%s', $this->testNode->id()));
$this->assertTasks('Edit draft');
// When published as the live revision, the label changes.
$this->testNode->moderation_state = 'published';
$this->testNode->save();
$this->drupalGet(sprintf('node/%s', $this->testNode->id()));
$this->assertTasks('New draft');
$tags = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertContains('node:1', $tags);
$this->assertContains('node_type:test_content_type', $tags);
// Without an upcast node, the state cannot be determined.
$this->clickLink('Task Without Upcast Node');
$this->assertTasks('Edit');
}
/**
* Assert the correct tasks appear.
*
* @param string $edit_tab_label
* The edit tab label to assert.
*/
protected function assertTasks($edit_tab_label) {
$this->assertSession()->linkExists('View');
$this->assertSession()->linkExists('Task Without Upcast Node');
$this->assertSession()->linkExists($edit_tab_label);
$this->assertSession()->linkExists('Delete');
}
}

View file

@ -0,0 +1,208 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\workflows\Entity\Workflow;
/**
* Tests the moderation form, specifically on nodes.
*
* @group content_moderation
*/
class ModerationFormTest extends ModerationStateTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
/**
* Tests the moderation form that shows on the latest version page.
*
* The latest version page only shows if there is a forward revision. There
* is only a forward revision if a draft revision is created on a node where
* the default revision is not a published moderation state.
*
* @see \Drupal\content_moderation\EntityOperations
* @see \Drupal\Tests\content_moderation\Functional\ModerationStateBlockTest::testCustomBlockModeration
*/
public function testModerationForm() {
// Create new moderated content in draft.
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'Some moderated content',
'body[0][value]' => 'First version of the content.',
], t('Save and Create New Draft'));
$node = $this->drupalGetNodeByTitle('Some moderated content');
$canonical_path = sprintf('node/%d', $node->id());
$edit_path = sprintf('node/%d/edit', $node->id());
$latest_version_path = sprintf('node/%d/latest', $node->id());
$this->assertTrue($this->adminUser->hasPermission('edit any moderated_content content'));
// The canonical view should have a moderation form, because it is not the
// live revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertField('edit-new-state', 'The node view page has a moderation form.');
// The latest version page should not show, because there is no forward
// revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Update the draft.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Second version of the content.',
], t('Save and Create New Draft'));
// The canonical view should have a moderation form, because it is not the
// live revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertField('edit-new-state', 'The node view page has a moderation form.');
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Publish the draft.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Third version of the content.',
], t('Save and Publish'));
// The published view should not have a moderation form, because it is the
// live revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertNoField('edit-new-state', 'The node view page has no moderation form.');
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
// Make a forward revision.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Fourth version of the content.',
], t('Save and Create New Draft'));
// The published view should not have a moderation form, because it is the
// live revision.
$this->drupalGet($canonical_path);
$this->assertResponse(200);
$this->assertNoField('edit-new-state', 'The node view page has no moderation form.');
// The latest version page should show the moderation form and have "Draft"
// status, because the forward revision is in "Draft".
$this->drupalGet($latest_version_path);
$this->assertResponse(200);
$this->assertField('edit-new-state', 'The latest-version page has a moderation form.');
$this->assertText('Draft', 'Correct status found on the latest-version page.');
// Submit the moderation form to change status to published.
$this->drupalPostForm($latest_version_path, [
'new_state' => 'published',
], t('Apply'));
// The latest version page should not show, because there is no
// forward revision.
$this->drupalGet($latest_version_path);
$this->assertResponse(403);
}
/**
* Test moderation non-bundle entity type.
*/
public function testNonBundleModerationForm() {
$this->drupalLogin($this->rootUser);
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
$workflow->save();
// Create new moderated content in draft.
$this->drupalPostForm('entity_test_mulrevpub/add', [], t('Save and Create New Draft'));
// The latest version page should not show, because there is no forward
// revision.
$this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
$this->assertResponse(403);
// Update the draft.
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
$this->assertResponse(403);
// Publish the draft.
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Publish'));
// The published view should not have a moderation form, because it is the
// default revision.
$this->drupalGet('entity_test_mulrevpub/manage/1');
$this->assertResponse(200);
$this->assertNoText('Status', 'The node view page has no moderation form.');
// The latest version page should not show, because there is still no
// forward revision.
$this->drupalGet('entity_test_mulrevpub/manage/1/latest');
$this->assertResponse(403);
// Make a forward revision.
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
// The published view should not have a moderation form, because it is the
// default revision.
$this->drupalGet('entity_test_mulrevpub/manage/1');
$this->assertResponse(200);
$this->assertNoText('Status', 'The node view page has no moderation form.');
// The latest version page should show the moderation form and have "Draft"
// status, because the forward revision is in "Draft".
$this->drupalGet('entity_test_mulrevpub/manage/1/latest');
$this->assertResponse(200);
$this->assertText('Status', 'Form text found on the latest-version page.');
$this->assertText('Draft', 'Correct status found on the latest-version page.');
// Submit the moderation form to change status to published.
$this->drupalPostForm('entity_test_mulrevpub/manage/1/latest', [
'new_state' => 'published',
], t('Apply'));
// The latest version page should not show, because there is no
// forward revision.
$this->drupalGet('entity_test_mulrevpub/manage/1/latest');
$this->assertResponse(403);
}
/**
* Tests the revision author is updated when the moderation form is used.
*/
public function testModerationFormSetsRevisionAuthor() {
// Create new moderated content in published.
$node = $this->createNode(['type' => 'moderated_content', 'moderation_state' => 'published']);
// Make a forward revision.
$node->title = $this->randomMachineName();
$node->moderation_state->value = 'draft';
$node->save();
$another_user = $this->drupalCreateUser($this->permissions);
$this->grantUserPermissionToCreateContentOfType($another_user, 'moderated_content');
$this->drupalLogin($another_user);
$this->drupalPostForm(sprintf('node/%d/latest', $node->id()), [
'new_state' => 'published',
], t('Apply'));
$this->drupalGet(sprintf('node/%d/revisions', $node->id()));
$this->assertText('by ' . $another_user->getAccountName());
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\simpletest\ContentTypeCreationTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Test revision revert.
*
* @group content_moderation
*/
class ModerationRevisionRevertTest extends BrowserTestBase {
use ContentTypeCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'content_moderation',
'node',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$moderated_bundle = $this->createContentType(['type' => 'moderated_bundle']);
$moderated_bundle->save();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
$workflow->save();
$admin = $this->drupalCreateUser([
'access content overview',
'administer nodes',
'bypass node access',
'view all revisions',
'view content moderation',
'use editorial transition create_new_draft',
'use editorial transition publish',
]);
$this->drupalLogin($admin);
}
/**
* Test that reverting a revision works.
*/
public function testEditingAfterRevertRevision() {
// Create a draft.
$this->drupalPostForm('node/add/moderated_bundle', ['title[0][value]' => 'First draft node'], t('Save and Create New Draft'));
// Now make it published.
$this->drupalPostForm('node/1/edit', ['title[0][value]' => 'Published node'], t('Save and Publish'));
// Check the editing form that show the published title.
$this->drupalGet('node/1/edit');
$this->assertSession()
->pageTextContains('Published node');
// Revert the first revision.
$revision_url = 'node/1/revisions/1/revert';
$this->drupalGet($revision_url);
$this->assertSession()->elementExists('css', '.form-submit');
$this->click('.form-submit');
// Check that it reverted.
$this->drupalGet('node/1/edit');
$this->assertSession()
->pageTextContains('First draft node');
// Try to save the node.
$this->click('.moderation-state-draft > input');
// Check if the submission passed the EntityChangedConstraintValidator.
$this->assertSession()
->pageTextNotContains('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
// Check the node has been saved.
$this->assertSession()
->pageTextContains('moderated_bundle First draft node has been updated');
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\content_moderation\Tests;
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\content_moderation\Tests;
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
@ -132,7 +132,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/content');
$element = $this->cssSelect('nav.pager li.is-active a');
$url = (string) $element[0]['href'];
$url = $element[0]->getAttribute('href');
$query = [];
parse_str(parse_url($url, PHP_URL_QUERY), $query);
$this->assertEqual(0, $query['page']);

View file

@ -1,7 +1,6 @@
<?php
namespace Drupal\content_moderation\Tests;
namespace Drupal\Tests\content_moderation\Functional;
/**
* Tests moderation state node type integration.
@ -30,10 +29,25 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase {
* Tests enabling moderation on an existing node-type, with content.
*/
public function testEnablingOnExistingContent() {
$editor_permissions = [
'administer content moderation',
'access administration pages',
'administer content types',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
'use editorial transition create_new_draft',
];
$publish_permissions = array_merge($editor_permissions, ['use editorial transition publish']);
$editor = $this->drupalCreateUser($editor_permissions);
$editor_with_publish = $this->drupalCreateUser($publish_permissions);
// Create a node type that is not moderated.
$this->drupalLogin($this->adminUser);
$this->drupalLogin($editor);
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
$this->grantUserPermissionToCreateContentOfType($editor, 'not_moderated');
$this->grantUserPermissionToCreateContentOfType($editor_with_publish, 'not_moderated');
// Create content.
$this->drupalGet('node/add/not_moderated');
@ -68,7 +82,13 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase {
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertResponse(200);
$this->assertRaw('Save and Create New Draft');
$this->assertNoRaw('Save and publish');
$this->assertNoRaw('Save and Publish');
$this->drupalLogin($editor_with_publish);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertResponse(200);
$this->assertRaw('Save and Create New Draft');
$this->assertRaw('Save and Publish');
}
}

View file

@ -50,6 +50,7 @@ abstract class ModerationStateTestBase extends BrowserTestBase {
'block',
'block_content',
'node',
'entity_test',
];
/**

View file

@ -9,6 +9,37 @@ namespace Drupal\Tests\content_moderation\Functional;
*/
class NodeAccessTest extends ModerationStateTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'content_moderation',
'block',
'block_content',
'node',
'node_access_test_empty',
];
/**
* Permissions to grant admin user.
*
* @var array
*/
protected $permissions = [
'administer content moderation',
'access administration pages',
'administer content types',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
'use editorial transition create_new_draft',
'use editorial transition publish',
'bypass node access',
];
/**
* {@inheritdoc}
*/
@ -17,6 +48,10 @@ class NodeAccessTest extends ModerationStateTestBase {
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
// Rebuild permissions because hook_node_grants() is implemented by the
// node_access_test_empty module.
node_access_rebuild();
}
/**
@ -38,7 +73,24 @@ class NodeAccessTest extends ModerationStateTestBase {
$edit_path = 'node/' . $node->id() . '/edit';
$latest_path = 'node/' . $node->id() . '/latest';
// Now make a new user and verify that the new user's access is correct.
$user = $this->createUser([
'use editorial transition create_new_draft',
'view latest version',
'view any unpublished content',
]);
$this->drupalLogin($user);
$this->drupalGet($edit_path);
$this->assertResponse(403);
$this->drupalGet($latest_path);
$this->assertResponse(403);
$this->drupalGet($view_path);
$this->assertResponse(200);
// Publish the node.
$this->drupalLogin($this->adminUser);
$this->drupalPostForm($edit_path, [], t('Save and Publish'));
// Ensure access works correctly for anonymous users.
@ -58,12 +110,6 @@ class NodeAccessTest extends ModerationStateTestBase {
'title[0][value]' => 'moderated content revised',
], t('Save and Create New Draft'));
// Now make a new user and verify that the new user's access is correct.
$user = $this->createUser([
'use editorial transition create_new_draft',
'view latest version',
'view any unpublished content',
]);
$this->drupalLogin($user);
$this->drupalGet($edit_path);

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\content_moderation\Entity\ContentModerationState;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\content_moderation\ContentModerationStateAccessControlHandler
* @group content_moderation
*/
class ContentModerationStateAccessControlHandlerTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'content_moderation',
'workflows',
'user',
];
/**
* The content_moderation_state access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessControlHandler;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('content_moderation_state');
$this->installEntitySchema('user');
$this->accessControlHandler = $this->container->get('entity_type.manager')->getAccessControlHandler('content_moderation_state');
}
/**
* @covers ::checkAccess
* @covers ::checkCreateAccess
*/
public function testHandler() {
$entity = ContentModerationState::create([]);
$this->assertFalse($this->accessControlHandler->access($entity, 'view'));
$this->assertFalse($this->accessControlHandler->access($entity, 'update'));
$this->assertFalse($this->accessControlHandler->access($entity, 'delete'));
$this->assertFalse($this->accessControlHandler->createAccess());
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\content_moderation\Entity\ContentModerationState;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\workflows\Entity\Workflow;
/**
* Test the ContentModerationState storage schema.
*
* @coversDefaultClass \Drupal\content_moderation\ContentModerationStateStorageSchema
* @group content_moderation
*/
class ContentModerationStateStorageSchemaTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'content_moderation',
'user',
'system',
'text',
'workflows',
'entity_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('node', 'node_access');
$this->installEntitySchema('node');
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
NodeType::create([
'type' => 'example',
])->save();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
}
/**
* Test the ContentModerationState unique keys.
*
* @covers ::getEntitySchema
*/
public function testUniqueKeys() {
// Create a node which will create a new ContentModerationState entity.
$node = Node::create([
'title' => 'Test title',
'type' => 'example',
'moderation_state' => 'draft',
]);
$node->save();
// Ensure an exception when all values match.
$this->assertStorageException([
'content_entity_type_id' => $node->getEntityTypeId(),
'content_entity_id' => $node->id(),
'content_entity_revision_id' => $node->getRevisionId(),
], TRUE);
// No exception for the same values, with a different langcode.
$this->assertStorageException([
'content_entity_type_id' => $node->getEntityTypeId(),
'content_entity_id' => $node->id(),
'content_entity_revision_id' => $node->getRevisionId(),
'langcode' => 'de',
], FALSE);
// A different workflow should not trigger an exception.
$this->assertStorageException([
'content_entity_type_id' => $node->getEntityTypeId(),
'content_entity_id' => $node->id(),
'content_entity_revision_id' => $node->getRevisionId(),
'workflow' => 'foo',
], FALSE);
// Different entity types should not trigger an exception.
$this->assertStorageException([
'content_entity_type_id' => 'entity_test',
'content_entity_id' => $node->id(),
'content_entity_revision_id' => $node->getRevisionId(),
], FALSE);
// Different entity and revision IDs should not trigger an exception.
$this->assertStorageException([
'content_entity_type_id' => $node->getEntityTypeId(),
'content_entity_id' => 9999,
'content_entity_revision_id' => 9999,
], FALSE);
// Creating a version of the entity with a previously used, but not current
// revision ID should trigger an exception.
$old_revision_id = $node->getRevisionId();
$node->setNewRevision(TRUE);
$node->title = 'Updated title';
$node->moderation_state = 'published';
$node->save();
$this->assertStorageException([
'content_entity_type_id' => $node->getEntityTypeId(),
'content_entity_id' => $node->id(),
'content_entity_revision_id' => $old_revision_id,
], TRUE);
}
/**
* Assert if a storage exception is triggered when saving a given entity.
*
* @param array $values
* An array of entity values.
* @param bool $has_exception
* If an exception should be triggered when saving the entity.
*/
protected function assertStorageException(array $values, $has_exception) {
$defaults = [
'moderation_state' => 'draft',
'workflow' => 'editorial',
];
$entity = ContentModerationState::create($values + $defaults);
$exception_triggered = FALSE;
try {
ContentModerationState::updateOrCreateFromEntity($entity);
}
catch (\Exception $e) {
$exception_triggered = TRUE;
}
$this->assertEquals($has_exception, $exception_triggered);
}
}

View file

@ -53,6 +53,7 @@ class ContentModerationStateTest extends KernelTestBase {
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test_with_bundle');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('entity_test_no_bundle');
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('block_content');
$this->installEntitySchema('content_moderation_state');
@ -94,6 +95,9 @@ class ContentModerationStateTest extends KernelTestBase {
'title' => 'Test title',
$this->entityTypeManager->getDefinition($entity_type_id)->getKey('bundle') => $bundle_id,
]);
if ($entity instanceof EntityPublishedInterface) {
$entity->setUnpublished();
}
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertEquals('draft', $entity->moderation_state->value);
@ -178,7 +182,10 @@ class ContentModerationStateTest extends KernelTestBase {
],
'Entity Test with revisions' => [
'entity_test_rev',
]
],
'Entity without bundle' => [
'entity_test_no_bundle',
],
];
}
@ -400,6 +407,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Test both a config and non-config based bundle and entity type.
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
$workflow->save();
$this->assertEquals([
@ -412,9 +420,11 @@ class ContentModerationStateTest extends KernelTestBase {
],
], $workflow->getDependencies());
$entity_types = $workflow->getTypePlugin()->getEntityTypes();
$this->assertTrue(in_array('node', $entity_types));
$this->assertTrue(in_array('entity_test_rev', $entity_types));
$this->assertEquals([
'entity_test_no_bundle',
'entity_test_rev',
'node'
], $workflow->getTypePlugin()->getEntityTypes());
// Delete the node type and ensure it is removed from the workflow.
$node_type->delete();
@ -426,7 +436,7 @@ class ContentModerationStateTest extends KernelTestBase {
$this->container->get('config.manager')->uninstall('module', 'entity_test');
$workflow = Workflow::load('editorial');
$entity_types = $workflow->getTypePlugin()->getEntityTypes();
$this->assertFalse(in_array('entity_test_rev', $entity_types));
$this->assertEquals([], $entity_types);
}
/**

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\workflows\Entity\Workflow;
/**
* Tests the correct default revision is set.
*
* @group content_moderation
*/
class DefaultRevisionStateTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'entity_test',
'node',
'block_content',
'content_moderation',
'user',
'system',
'language',
'content_translation',
'text',
'workflows',
];
/**
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('node', 'node_access');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test_with_bundle');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('block_content');
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests a translatable Node.
*/
public function testMultilingual() {
// Enable French.
ConfigurableLanguage::createFromLangcode('fr')->save();
$node_type = NodeType::create([
'type' => 'example',
]);
$node_type->save();
$this->container->get('content_translation.manager')->setEnabled('node', 'example', TRUE);
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
$english_node = Node::create([
'type' => 'example',
'title' => 'Test title',
]);
// Revision 1 (en).
$english_node
->setUnpublished()
->save();
$this->assertEquals('draft', $english_node->moderation_state->value);
$this->assertFalse($english_node->isPublished());
$this->assertTrue($english_node->isDefaultRevision());
// Revision 2 (fr)
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
$french_node->moderation_state->value = 'published';
$french_node->save();
$this->assertTrue($french_node->isPublished());
$this->assertTrue($french_node->isDefaultRevision());
// Revision 3 (fr)
$node = Node::load($english_node->id())->getTranslation('fr');
$node->moderation_state->value = 'draft';
$node->save();
$this->assertFalse($node->isPublished());
$this->assertFalse($node->isDefaultRevision());
// Revision 4 (en)
$latest_revision = $this->entityTypeManager->getStorage('node')->loadRevision(3);
$latest_revision->moderation_state->value = 'draft';
$latest_revision->save();
$this->assertFalse($latest_revision->isPublished());
$this->assertFalse($latest_revision->isDefaultRevision());
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\workflows\Entity\Workflow;
/**
* Tests the correct initial states are set on install.
*
* @group content_moderation
*/
class InitialStateTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'entity_test',
'node',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('node', 'node_access');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test_rev');
}
/**
* Tests the correct initial state.
*/
public function testInitialState() {
$node_type = NodeType::create([
'type' => 'example',
]);
$node_type->save();
// Test with an entity type that implements EntityPublishedInterface.
$unpublished_node = Node::create([
'type' => 'example',
'title' => 'Unpublished node',
'status' => 0,
]);
$unpublished_node->save();
$published_node = Node::create([
'type' => 'example',
'title' => 'Published node',
'status' => 1,
]);
$published_node->save();
// Test with an entity type that doesn't implement EntityPublishedInterface.
$entity_test = EntityTestRev::create();
$entity_test->save();
\Drupal::service('module_installer')->install(['content_moderation'], TRUE);
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
$workflow->save();
$loaded_unpublished_node = Node::load($unpublished_node->id());
$loaded_published_node = Node::load($published_node->id());
$loaded_entity_test = EntityTestRev::load($entity_test->id());
$this->assertEquals('draft', $loaded_unpublished_node->moderation_state->value);
$this->assertEquals('published', $loaded_published_node->moderation_state->value);
$this->assertEquals('draft', $loaded_entity_test->moderation_state->value);
}
}

View file

@ -64,7 +64,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
* Test the field item list when accessing an index.
*/
public function testArrayIndex() {
$this->assertEquals('draft', $this->testNode->moderation_state[0]->value);
$this->assertEquals('published', $this->testNode->moderation_state[0]->value);
}
/**
@ -75,7 +75,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
foreach ($this->testNode->moderation_state as $item) {
$states[] = $item->value;
}
$this->assertEquals(['draft'], $states);
$this->assertEquals(['published'], $states);
}
}

View file

@ -5,10 +5,16 @@ namespace Drupal\Tests\content_moderation\Unit;
use Drupal\block_content\Entity\BlockContent;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\Entity\Node;
use Drupal\content_moderation\Access\LatestRevisionCheck;
use Drupal\content_moderation\ModerationInformation;
use Drupal\user\EntityOwnerInterface;
use Prophecy\Argument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\Route;
/**
@ -17,6 +23,20 @@ use Symfony\Component\Routing\Route;
*/
class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Initialize Drupal container since the cache context manager is needed.
$contexts_manager = $this->prophesize(CacheContextsManager::class);
$contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE);
$builder = new ContainerBuilder();
$builder->set('cache_contexts_manager', $contexts_manager->reveal());
\Drupal::setContainer($builder);
}
/**
* Test the access check of the LatestRevisionCheck service.
*
@ -26,19 +46,38 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
* The machine name of the entity to mock.
* @param bool $has_forward
* Whether this entity should have a forward revision in the system.
* @param array $account_permissions
* An array of permissions the account has.
* @param bool $is_owner
* Indicates if the user should be the owner of the entity.
* @param string $result_class
* The AccessResult class that should result. One of AccessResultAllowed,
* AccessResultForbidden, AccessResultNeutral.
*
* @dataProvider accessSituationProvider
*/
public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, $result_class) {
public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, array $account_permissions, $is_owner, $result_class) {
/** @var \Drupal\Core\Session\AccountInterface $account */
$account = $this->prophesize(AccountInterface::class);
$possible_permissions = [
'view latest version',
'view any unpublished content',
'view own unpublished content',
];
foreach ($possible_permissions as $permission) {
$account->hasPermission($permission)->willReturn(in_array($permission, $account_permissions));
}
$account->id()->willReturn(42);
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $this->prophesize($entity_class);
$entity->getCacheContexts()->willReturn([]);
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(0);
if (is_subclass_of($entity_class, EntityOwnerInterface::class)) {
$entity->getOwnerId()->willReturn($is_owner ? 42 : 3);
}
/** @var \Drupal\content_moderation\ModerationInformation $mod_info */
$mod_info = $this->prophesize(ModerationInformation::class);
@ -54,7 +93,7 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
$lrc = new LatestRevisionCheck($mod_info->reveal());
/** @var \Drupal\Core\Access\AccessResult $result */
$result = $lrc->access($route->reveal(), $route_match->reveal());
$result = $lrc->access($route->reveal(), $route_match->reveal(), $account->reveal());
$this->assertInstanceOf($result_class, $result);
@ -65,10 +104,28 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
*/
public function accessSituationProvider() {
return [
[Node::class, 'node', TRUE, AccessResultAllowed::class],
[Node::class, 'node', FALSE, AccessResultForbidden::class],
[BlockContent::class, 'block_content', TRUE, AccessResultAllowed::class],
[BlockContent::class, 'block_content', FALSE, AccessResultForbidden::class],
// Node with global permissions and latest version.
[Node::class, 'node', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class],
// Node with global permissions and no latest version.
[Node::class, 'node', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class],
// Node with own content permissions and latest version.
[Node::class, 'node', TRUE, ['view latest version', 'view own unpublished content'], TRUE, AccessResultAllowed::class],
// Node with own content permissions and no latest version.
[Node::class, 'node', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class],
// Node with own content permissions and latest version, but no perms to
// view latest version.
[Node::class, 'node', TRUE, ['view own unpublished content'], TRUE, AccessResultNeutral::class],
// Node with own content permissions and no latest version, but no perms
// to view latest version.
[Node::class, 'node', TRUE, ['view own unpublished content'], FALSE, AccessResultNeutral::class],
// Block with forward revision, and permissions to view any.
[BlockContent::class, 'block_content', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class],
// Block with no forward revision.
[BlockContent::class, 'block_content', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class],
// Block with forward revision, but no permission to view any.
[BlockContent::class, 'block_content', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class],
// Block with no forward revision.
[BlockContent::class, 'block_content', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class],
];
}