summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLee Rowlands2017-07-28 09:07:55 +1000
committerLee Rowlands2017-07-28 09:07:55 +1000
commit832d7695acaff7c557d0a0b607a2676d983b36ac (patch)
tree0441fdcb7db55509ed58610a12f3a329a5878f4d
parente6cd4b0dc20f9bff638bee6c8c327477f7c38988 (diff)
Issue #2830740 by timmillwood, amateescu, Sam152, alexpott, martin107, plach, catch: Allow workflow types to lock certain changes to workflows once things are in use
-rw-r--r--core/modules/content_moderation/content_moderation.services.yml7
-rw-r--r--core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php96
-rw-r--r--core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php29
-rw-r--r--core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php49
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php132
-rw-r--r--core/modules/workflows/src/Form/WorkflowDeleteForm.php13
-rw-r--r--core/modules/workflows/src/Form/WorkflowStateDeleteForm.php7
-rw-r--r--core/modules/workflows/src/Plugin/WorkflowTypeBase.php14
-rw-r--r--core/modules/workflows/src/WorkflowTypeInterface.php32
9 files changed, 378 insertions, 1 deletions
diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml
index 904bc0d..256095a 100644
--- a/core/modules/content_moderation/content_moderation.services.yml
+++ b/core/modules/content_moderation/content_moderation.services.yml
@@ -19,4 +19,9 @@ services:
class: Drupal\content_moderation\RevisionTracker
arguments: ['@database']
tags:
- - { name: backend_overridable }
+ - { name: backend_overridable }
+ content_moderation.config_import_subscriber:
+ class: Drupal\content_moderation\EventSubscriber\ConfigImportSubscriber
+ arguments: ['@config.manager', '@entity_type.manager']
+ tags:
+ - { name: event_subscriber }
diff --git a/core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php b/core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
new file mode 100644
index 0000000..bb7d680
--- /dev/null
+++ b/core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\content_moderation\EventSubscriber;
+
+use Drupal\Core\Config\ConfigImporterEvent;
+use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
+use Drupal\Core\Config\ConfigManagerInterface;
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Check moderation states are not being used before updating workflow config.
+ */
+class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
+
+ /**
+ * The config manager.
+ *
+ * @var \Drupal\Core\Config\ConfigManagerInterface
+ */
+ protected $configManager;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs the event subscriber.
+ *
+ * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
+ * The config manager
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
+ public function __construct(ConfigManagerInterface $config_manager, EntityTypeManagerInterface $entity_type_manager) {
+ $this->configManager = $config_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onConfigImporterValidate(ConfigImporterEvent $event) {
+ foreach (['update', 'delete'] as $op) {
+ $unprocessed_configurations = $event->getConfigImporter()->getUnprocessedConfiguration($op);
+ foreach ($unprocessed_configurations as $unprocessed_configuration) {
+ if ($workflow = $this->getWorkflow($unprocessed_configuration)) {
+ if ($op === 'update') {
+ $original_workflow_config = $event->getConfigImporter()
+ ->getStorageComparer()
+ ->getSourceStorage()
+ ->read($unprocessed_configuration);
+ $workflow_config = $event->getConfigImporter()
+ ->getStorageComparer()
+ ->getTargetStorage()
+ ->read($unprocessed_configuration);
+ $diff = array_diff_key($workflow_config['type_settings']['states'], $original_workflow_config['type_settings']['states']);
+ foreach (array_keys($diff) as $state_id) {
+ $state = $workflow->getState($state_id);
+ if ($workflow->getTypePlugin()->workflowStateHasData($workflow, $state)) {
+ $event->getConfigImporter()->logError($this->t('The moderation state @state_label is being used, but is not in the source storage.', ['@state_label' => $state->label()]));
+ }
+ }
+ }
+ if ($op === 'delete') {
+ if ($workflow->getTypePlugin()->workflowHasData($workflow)) {
+ $event->getConfigImporter()->logError($this->t('The workflow @workflow_label is being used, and cannot be deleted.', ['@workflow_label' => $workflow->label()]));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the workflow entity object from the configuration name.
+ *
+ * @param string $config_name
+ * The configuration object name.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * An entity object. NULL if no matching entity is found.
+ */
+ protected function getWorkflow($config_name) {
+ $entity_type_id = $this->configManager->getEntityTypeIdByName($config_name);
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+ $entity_id = ConfigEntityStorage::getIDFromConfigName($config_name, $entity_type->getConfigPrefix());
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ return $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
+ }
+
+}
diff --git a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
index c45803a..f4bbef5 100644
--- a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
+++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
@@ -117,6 +117,35 @@ class ContentModeration extends WorkflowTypeFormBase implements ContainerFactory
/**
* {@inheritdoc}
*/
+ public function workflowHasData(WorkflowInterface $workflow) {
+ return (bool) $this->entityTypeManager
+ ->getStorage('content_moderation_state')
+ ->getQuery()
+ ->condition('workflow', $workflow->id())
+ ->count()
+ ->accessCheck(FALSE)
+ ->range(0, 1)
+ ->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
+ return (bool) $this->entityTypeManager
+ ->getStorage('content_moderation_state')
+ ->getQuery()
+ ->condition('workflow', $workflow->id())
+ ->condition('moderation_state', $state->id())
+ ->count()
+ ->accessCheck(FALSE)
+ ->range(0, 1)
+ ->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
/** @var \Drupal\content_moderation\ContentModerationState $state */
$is_required_state = isset($state) ? in_array($state->id(), $this->getRequiredStates(), TRUE) : FALSE;
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
index 7aab0d4..b6fe577 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
@@ -451,4 +451,53 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalPostForm(NULL, [], t('Save and Create New Draft (this translation)'));
}
+ /**
+ * Tests that workflows and states can not be deleted if they are in use.
+ *
+ * @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowHasData
+ * @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowStateHasData
+ */
+ public function testWorkflowInUse() {
+ $user = $this->createUser([
+ 'administer workflows',
+ 'create moderated_content content',
+ 'edit own moderated_content content',
+ 'use editorial transition create_new_draft',
+ 'use editorial transition publish',
+ 'use editorial transition archive'
+ ]);
+ $this->drupalLogin($user);
+ $paths = [
+ 'archived_state' => 'admin/config/workflow/workflows/manage/editorial/state/archived/delete',
+ 'editorial_workflow' => 'admin/config/workflow/workflows/manage/editorial/delete',
+ ];
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertSession()->buttonExists('Delete');
+ }
+ // 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.',
+ ], 'Save and Create New Draft');
+
+ // The archived state is not used yet, so can still be deleted.
+ $this->drupalGet($paths['archived_state']);
+ $this->assertSession()->buttonExists('Delete');
+
+ // The workflow is being used, so can't be deleted.
+ $this->drupalGet($paths['editorial_workflow']);
+ $this->assertSession()->buttonNotExists('Delete');
+
+ $node = $this->drupalGetNodeByTitle('Some moderated content');
+ $this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Save and Publish');
+ $this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Save and Archive');
+
+ // Now the archived state is being used so it can not be deleted either.
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $this->assertSession()->buttonNotExists('Delete');
+ }
+ }
+
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php
new file mode 100644
index 0000000..90ecfcf
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace Drupal\Tests\content_moderation\Kernel;
+
+use Drupal\Core\Config\ConfigImporterException;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests how Content Moderation handles workflow config changes.
+ *
+ * @group content_moderation
+ */
+class ContentModerationWorkflowConfigTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'node',
+ 'content_moderation',
+ 'user',
+ 'system',
+ 'text',
+ 'workflows',
+ ];
+
+ /**
+ * @var \Drupal\Core\Entity\EntityTypeManager
+ */
+ protected $entityTypeManager;
+
+ /**
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
+ */
+ protected $configFactory;
+
+ /**
+ * @var \Drupal\workflows\Entity\Workflow
+ */
+ protected $workflow;
+
+ /**
+ * @var \Drupal\Core\Config\Entity\ConfigEntityStorage
+ */
+ protected $workflowStorage;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->installSchema('node', 'node_access');
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('content_moderation_state');
+ $this->installConfig('content_moderation');
+
+ NodeType::create([
+ 'type' => 'example',
+ ])->save();
+
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()
+ ->addState('test1', 'Test one')
+ ->addState('test2', 'Test two')
+ ->addState('test3', 'Test three')
+ ->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+ $this->workflow = $workflow;
+
+ $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
+ }
+
+ /**
+ * Test deleting a state via config import.
+ */
+ public function testDeletingStateViaConfiguration() {
+ $config_data = $this->config('workflows.workflow.editorial')->get();
+ unset($config_data['type_settings']['states']['test1']);
+ \Drupal::service('config.storage.sync')->write('workflows.workflow.editorial', $config_data);
+
+ // There are no Nodes with the moderation state test1, so this should run
+ // with no errors.
+ $this->configImporter()->reset()->import();
+
+ $node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Test title',
+ 'moderation_state' => 'test2',
+ ]);
+ $node->save();
+
+ $config_data = $this->config('workflows.workflow.editorial')->get();
+ unset($config_data['type_settings']['states']['test2']);
+ unset($config_data['type_settings']['states']['test3']);
+ \Drupal::service('config.storage.sync')->write('workflows.workflow.editorial', $config_data);
+
+ // Now there is a Node with the moderation state test2, this will fail.
+ try {
+ $this->configImporter()->reset()->import();
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted state.');
+ }
+ catch (ConfigImporterException $e) {
+ $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
+ $error_log = $this->configImporter->getErrors();
+ $expected = ['The moderation state Test two is being used, but is not in the source storage.'];
+ $this->assertEqual($expected, $error_log);
+ }
+
+ \Drupal::service('config.storage.sync')->delete('workflows.workflow.editorial');
+
+ // An error should be thrown when trying to delete an in use workflow.
+ try {
+ $this->configImporter()->reset()->import();
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted workflow.');
+ }
+ catch (ConfigImporterException $e) {
+ $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
+ $error_log = $this->configImporter->getErrors();
+ $expected = [
+ 'The moderation state Test two is being used, but is not in the source storage.',
+ 'The workflow Editorial workflow is being used, and cannot be deleted.',
+ ];
+ $this->assertEqual($expected, $error_log);
+ }
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowDeleteForm.php b/core/modules/workflows/src/Form/WorkflowDeleteForm.php
index b9b8331..e122f40 100644
--- a/core/modules/workflows/src/Form/WorkflowDeleteForm.php
+++ b/core/modules/workflows/src/Form/WorkflowDeleteForm.php
@@ -14,6 +14,19 @@ class WorkflowDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ if ($this->entity->getTypePlugin()->workflowHasData($this->entity)) {
+ $form['#title'] = $this->getQuestion();
+ $form['description'] = ['#markup' => $this->t('This workflow is in use. You cannot remove this workflow until you have removed all content using it.')];
+ return $form;
+ }
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function getQuestion() {
return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
}
diff --git a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
index c60045c..d2c30d7 100644
--- a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
@@ -75,6 +75,13 @@ class WorkflowStateDeleteForm extends ConfirmFormBase {
}
$this->workflow = $workflow;
$this->stateId = $workflow_state;
+
+ if ($this->workflow->getTypePlugin()->workflowStateHasData($this->workflow, $this->workflow->getState($this->stateId))) {
+ $form['#title'] = $this->getQuestion();
+ $form['description'] = ['#markup' => $this->t('This workflow state is in use. You cannot remove this workflow state until you have removed all content using it.')];
+ return $form;
+ }
+
return parent::buildForm($form, $form_state);
}
diff --git a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
index 0c4a34f..efdfe7e 100644
--- a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
+++ b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
@@ -64,6 +64,20 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
/**
* {@inheritdoc}
*/
+ public function workflowHasData(WorkflowInterface $workflow) {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function decorateState(StateInterface $state) {
return $state;
}
diff --git a/core/modules/workflows/src/WorkflowTypeInterface.php b/core/modules/workflows/src/WorkflowTypeInterface.php
index d84d1a7..6ff8832 100644
--- a/core/modules/workflows/src/WorkflowTypeInterface.php
+++ b/core/modules/workflows/src/WorkflowTypeInterface.php
@@ -57,6 +57,38 @@ interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeIns
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account);
/**
+ * Determines if the workflow is being has data associated with it.
+ *
+ * @internal
+ * Marked as internal until it's validated this should form part of the
+ * public API in https://www.drupal.org/node/2897148.
+ *
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow to check.
+ *
+ * @return bool
+ * TRUE if the workflow is being used, FALSE if not.
+ */
+ public function workflowHasData(WorkflowInterface $workflow);
+
+ /**
+ * Determines if the workflow state has data associated with it.
+ *
+ * @internal
+ * Marked as internal until it's validated this should form part of the
+ * public API in https://www.drupal.org/node/2897148.
+ *
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow to check.
+ * @param \Drupal\workflows\StateInterface $state
+ * The workflow state to check.
+ *
+ * @return bool
+ * TRUE if the workflow state is being used, FALSE if not.
+ */
+ public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state);
+
+ /**
* Decorates states so the WorkflowType can add additional information.
*
* @param \Drupal\workflows\StateInterface $state