Skip to content
Snippets Groups Projects
Commit 8bedf88b authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2779931 by Sam152, alexpott, timmillwood, Wim Leers, catch, dawehner:...

Issue #2779931 by Sam152, alexpott, timmillwood, Wim Leers, catch, dawehner: Add storage exception that enforces unique content_entity_type_id and content_entity_id on the content moderation state content entity, and add access control handler to forbid all access
parent 3b22e70c
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
<?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.');
}
}
...@@ -16,11 +16,20 @@ class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema ...@@ -16,11 +16,20 @@ class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset); $schema = parent::getEntitySchema($entity_type, $reset);
// Creates an index to ensure that the lookup in // Creates unique keys to guarantee the integrity of the entity and to make
// \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList::getModerationState() // the lookup in ModerationStateFieldItemList::getModerationState() fast.
// is performant. $unique_keys = [
$schema['content_moderation_state_field_data']['indexes'] += [ 'content_entity_type_id',
'content_moderation_state__lookup' => ['content_entity_type_id', 'content_entity_id', 'content_entity_revision_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; return $schema;
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
* handlers = { * handlers = {
* "storage_schema" = "Drupal\content_moderation\ContentModerationStateStorageSchema", * "storage_schema" = "Drupal\content_moderation\ContentModerationStateStorageSchema",
* "views_data" = "\Drupal\views\EntityViewsData", * "views_data" = "\Drupal\views\EntityViewsData",
* "access" = "Drupal\content_moderation\ContentModerationStateAccessControlHandler",
* }, * },
* base_table = "content_moderation_state", * base_table = "content_moderation_state",
* revision_table = "content_moderation_state_revision", * revision_table = "content_moderation_state_revision",
...@@ -74,6 +75,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ...@@ -74,6 +75,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setLabel(t('Content entity type ID')) ->setLabel(t('Content entity type ID'))
->setDescription(t('The ID of the content entity type this moderation state is for.')) ->setDescription(t('The ID of the content entity type this moderation state is for.'))
->setRequired(TRUE) ->setRequired(TRUE)
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH)
->setRevisionable(TRUE); ->setRevisionable(TRUE);
$fields['content_entity_id'] = BaseFieldDefinition::create('integer') $fields['content_entity_id'] = BaseFieldDefinition::create('integer')
...@@ -82,10 +84,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ...@@ -82,10 +84,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setRequired(TRUE) ->setRequired(TRUE)
->setRevisionable(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') $fields['content_entity_revision_id'] = BaseFieldDefinition::create('integer')
->setLabel(t('Content entity revision ID')) ->setLabel(t('Content entity revision ID'))
->setDescription(t('The revision ID of the content entity this moderation state is for.')) ->setDescription(t('The revision ID of the content entity this moderation state is for.'))
......
...@@ -182,8 +182,9 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { ...@@ -182,8 +182,9 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) {
]); ]);
$content_moderation_state->workflow->target_id = $workflow->id(); $content_moderation_state->workflow->target_id = $workflow->id();
} }
else { elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
// Create a new revision. // If a new revision of the content has been created, add a new content
// moderation state revision.
$content_moderation_state->setNewRevision(TRUE); $content_moderation_state->setNewRevision(TRUE);
} }
......
<?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());
}
}
<?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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment