summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreffulgentsia2018-07-16 17:03:55 (GMT)
committereffulgentsia2018-07-16 17:05:59 (GMT)
commit0e4ef18c049c0416603500b18d5dae0097466e41 (patch)
treed01246e1ef3623fb15b6c4f712e0c9dcae84e3e5
parentf0372ebff9c056343bae628f835f0e340149208c (diff)
Issue #2975334 by amateescu, Wim Leers, plach, effulgentsia, webchick, timmillwood, yoroy: Prevent changes that would leak into the Live workspace
(cherry picked from commit 4ea4c13f7bd3bbf37e4354046edeccd011afe6f2)
-rw-r--r--core/modules/workspace/src/EntityOperations.php76
-rw-r--r--core/modules/workspace/src/Form/WorkspaceActivateForm.php2
-rw-r--r--core/modules/workspace/src/Form/WorkspaceDeleteForm.php2
-rw-r--r--core/modules/workspace/src/Form/WorkspaceDeployForm.php2
-rw-r--r--core/modules/workspace/src/Form/WorkspaceForm.php2
-rw-r--r--core/modules/workspace/src/Form/WorkspaceFormInterface.php12
-rw-r--r--core/modules/workspace/src/Form/WorkspaceSwitcherForm.php2
-rw-r--r--core/modules/workspace/src/FormOperations.php120
-rw-r--r--core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php57
-rw-r--r--core/modules/workspace/workspace.module20
10 files changed, 269 insertions, 26 deletions
diff --git a/core/modules/workspace/src/EntityOperations.php b/core/modules/workspace/src/EntityOperations.php
index 24887ad..0b3cc9e 100644
--- a/core/modules/workspace/src/EntityOperations.php
+++ b/core/modules/workspace/src/EntityOperations.php
@@ -3,9 +3,9 @@
namespace Drupal\workspace;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -119,13 +119,21 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_entity_presave()
*/
public function entityPresave(EntityInterface $entity) {
- /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
- // Only run if the entity type can belong to a workspace and we are in a
- // non-default workspace.
- if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) {
+ $entity_type = $entity->getEntityType();
+
+ // Only run if this is not an entity type provided by the Workspace module
+ // and we are in a non-default workspace
+ if ($entity_type->getProvider() === 'workspace' || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
return;
}
+ // Disallow any change to an unsupported entity when we are not in the
+ // default workspace.
+ if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
+ throw new \RuntimeException('This entity can only be saved in the default workspace.');
+ }
+
+ /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
if (!$entity->isNew() && !isset($entity->_isReplicating)) {
// Force a new revision if the entity is not replicating.
$entity->setNewRevision(TRUE);
@@ -209,6 +217,30 @@ class EntityOperations implements ContainerInjectionInterface {
}
/**
+ * Acts on an entity before it is deleted.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity being deleted.
+ *
+ * @see hook_entity_predelete()
+ */
+ public function entityPredelete(EntityInterface $entity) {
+ $entity_type = $entity->getEntityType();
+
+ // Only run if this is not an entity type provided by the Workspace module
+ // and we are in a non-default workspace
+ if ($entity_type->getProvider() === 'workspace' || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
+ return;
+ }
+
+ // Disallow any change to an unsupported entity when we are not in the
+ // default workspace.
+ if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
+ throw new \RuntimeException('This entity can only be deleted in the default workspace.');
+ }
+ }
+
+ /**
* Updates or creates a WorkspaceAssociation entity for a given entity.
*
* If the passed-in entity can belong to a workspace and already has a
@@ -266,15 +298,26 @@ class EntityOperations implements ContainerInjectionInterface {
*
* @see hook_form_alter()
*/
- public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
- $form_object = $form_state->getFormObject();
- if (!$form_object instanceof EntityFormInterface) {
+ public function entityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $entity = $form_state->getFormObject()->getEntity();
+ if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) {
return;
}
- $entity = $form_object->getEntity();
- if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) {
- return;
+ // For supported entity types, signal the fact that this form is safe to use
+ // in a non-default workspace.
+ // @see \Drupal\workspace\FormOperations::validateForm()
+ $form_state->set('workspace_safe', TRUE);
+
+ // Add an entity builder to the form which marks the edited entity object as
+ // a pending revision. This is needed so validation constraints like
+ // \Drupal\path\Plugin\Validation\Constraint\PathAliasConstraintValidator
+ // know in advance (before hook_entity_presave()) that the new revision will
+ // be a pending one.
+ $active_workspace = $this->workspaceManager->getActiveWorkspace();
+ if (!$active_workspace->isDefaultWorkspace()) {
+ $form['#entity_builders'][] = [$this, 'entityFormEntityBuild'];
}
/** @var \Drupal\workspace\WorkspaceAssociationStorageInterface $workspace_association_storage */
@@ -283,7 +326,7 @@ class EntityOperations implements ContainerInjectionInterface {
// An entity can only be edited in one workspace.
$workspace_id = reset($workspace_ids);
- if ($workspace_id !== $this->workspaceManager->getActiveWorkspace()->id()) {
+ if ($workspace_id !== $active_workspace->id()) {
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
$form['#markup'] = $this->t('The content is being edited in the %label workspace.', ['%label' => $workspace->label()]);
@@ -292,4 +335,13 @@ class EntityOperations implements ContainerInjectionInterface {
}
}
+ /**
+ * Entity builder that marks all supported entities as pending revisions.
+ */
+ public function entityFormEntityBuild($entity_type_id, RevisionableInterface $entity, &$form, FormStateInterface &$form_state) {
+ // Set the non-default revision flag so that validation constraints are also
+ // aware that a pending revision is about to be created.
+ $entity->isDefaultRevision(FALSE);
+ }
+
}
diff --git a/core/modules/workspace/src/Form/WorkspaceActivateForm.php b/core/modules/workspace/src/Form/WorkspaceActivateForm.php
index ea029c4..5fd9488 100644
--- a/core/modules/workspace/src/Form/WorkspaceActivateForm.php
+++ b/core/modules/workspace/src/Form/WorkspaceActivateForm.php
@@ -12,7 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Handle activation of a workspace on administrative pages.
*/
-class WorkspaceActivateForm extends EntityConfirmFormBase {
+class WorkspaceActivateForm extends EntityConfirmFormBase implements WorkspaceFormInterface {
/**
* The workspace entity.
diff --git a/core/modules/workspace/src/Form/WorkspaceDeleteForm.php b/core/modules/workspace/src/Form/WorkspaceDeleteForm.php
index 9c2113b..75e7c53 100644
--- a/core/modules/workspace/src/Form/WorkspaceDeleteForm.php
+++ b/core/modules/workspace/src/Form/WorkspaceDeleteForm.php
@@ -10,7 +10,7 @@ use Drupal\Core\Form\FormStateInterface;
*
* @internal
*/
-class WorkspaceDeleteForm extends ContentEntityDeleteForm {
+class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFormInterface {
/**
* The workspace entity.
diff --git a/core/modules/workspace/src/Form/WorkspaceDeployForm.php b/core/modules/workspace/src/Form/WorkspaceDeployForm.php
index bc24e20..2102ba8 100644
--- a/core/modules/workspace/src/Form/WorkspaceDeployForm.php
+++ b/core/modules/workspace/src/Form/WorkspaceDeployForm.php
@@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the workspace deploy form.
*/
-class WorkspaceDeployForm extends ContentEntityForm {
+class WorkspaceDeployForm extends ContentEntityForm implements WorkspaceFormInterface {
/**
* The workspace entity.
diff --git a/core/modules/workspace/src/Form/WorkspaceForm.php b/core/modules/workspace/src/Form/WorkspaceForm.php
index c0085b4..e6a3e0a 100644
--- a/core/modules/workspace/src/Form/WorkspaceForm.php
+++ b/core/modules/workspace/src/Form/WorkspaceForm.php
@@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the workspace edit forms.
*/
-class WorkspaceForm extends ContentEntityForm {
+class WorkspaceForm extends ContentEntityForm implements WorkspaceFormInterface {
/**
* The workspace entity.
diff --git a/core/modules/workspace/src/Form/WorkspaceFormInterface.php b/core/modules/workspace/src/Form/WorkspaceFormInterface.php
new file mode 100644
index 0000000..c91a0a8
--- /dev/null
+++ b/core/modules/workspace/src/Form/WorkspaceFormInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\workspace\Form;
+
+use Drupal\Core\Form\FormInterface;
+
+/**
+ * Defines interface for workspace forms so they can be easily distinguished.
+ *
+ * @internal
+ */
+interface WorkspaceFormInterface extends FormInterface {}
diff --git a/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php b/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php
index 6ac54a2..d726ad0 100644
--- a/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php
+++ b/core/modules/workspace/src/Form/WorkspaceSwitcherForm.php
@@ -13,7 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that activates a different workspace.
*/
-class WorkspaceSwitcherForm extends FormBase {
+class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
/**
* The workspace manager.
diff --git a/core/modules/workspace/src/FormOperations.php b/core/modules/workspace/src/FormOperations.php
new file mode 100644
index 0000000..95d6c87
--- /dev/null
+++ b/core/modules/workspace/src/FormOperations.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\workspace;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\views\Form\ViewsExposedForm;
+use Drupal\workspace\Form\WorkspaceFormInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a class for reacting to form operations.
+ *
+ * @internal
+ */
+class FormOperations implements ContainerInjectionInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The workspace manager service.
+ *
+ * @var \Drupal\workspace\WorkspaceManagerInterface
+ */
+ protected $workspaceManager;
+
+ /**
+ * Constructs a new FormOperations instance.
+ *
+ * @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
+ * The workspace manager service.
+ */
+ public function __construct(WorkspaceManagerInterface $workspace_manager) {
+ $this->workspaceManager = $workspace_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('workspace.manager')
+ );
+ }
+
+ /**
+ * Alters forms to disallow editing in non-default workspaces.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param string $form_id
+ * The form ID.
+ *
+ * @see hook_form_alter()
+ */
+ public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
+ // No alterations are needed in the default workspace.
+ if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
+ return;
+ }
+
+ // Add an additional validation step for every form if we are in a
+ // non-default workspace.
+ $this->addWorkspaceValidation($form);
+
+ // If a form has already been marked as safe or not to submit in a
+ // non-default workspace, we don't have anything else to do.
+ if ($form_state->has('workspace_safe')) {
+ return;
+ }
+
+ // No forms are safe to submit in a non-default workspace by default, except
+ // for the whitelisted ones defined below.
+ $workspace_safe = FALSE;
+
+ // Whitelist a few forms that we know are safe to submit.
+ $form_object = $form_state->getFormObject();
+ $is_workspace_form = $form_object instanceof WorkspaceFormInterface;
+ $is_search_form = in_array($form_object->getFormId(), ['search_block_form', 'search_form'], TRUE);
+ $is_views_exposed_form = $form_object instanceof ViewsExposedForm;
+ if ($is_workspace_form || $is_search_form || $is_views_exposed_form) {
+ $workspace_safe = TRUE;
+ }
+
+ $form_state->set('workspace_safe', $workspace_safe);
+ }
+
+ /**
+ * Adds our validation handler recursively on each element of a form.
+ *
+ * @param array &$element
+ * An associative array containing the structure of the form.
+ */
+ protected function addWorkspaceValidation(array &$element) {
+ // Recurse through all children and add our validation handler if needed.
+ foreach (Element::children($element) as $key) {
+ if (isset($element[$key]) && $element[$key]) {
+ $this->addWorkspaceValidation($element[$key]);
+ }
+ }
+
+ if (isset($element['#validate'])) {
+ $element['#validate'][] = [$this, 'validateDefaultWorkspace'];
+ }
+ }
+
+ /**
+ * Validation handler which sets a validation error for all unsupported forms.
+ */
+ public function validateDefaultWorkspace(array &$form, FormStateInterface $form_state) {
+ if ($form_state->get('workspace_safe') !== TRUE) {
+ $form_state->setError($form, $this->t('This form can only be submitted in the default workspace.'));
+ }
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php
index cbf2b74..fbd1f31 100644
--- a/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php
+++ b/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php
@@ -2,7 +2,9 @@
namespace Drupal\Tests\workspace\Kernel;
+use Drupal\Core\Entity\EntityStorageException;
use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
@@ -74,6 +76,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$this->installSchema('node', ['node_access']);
$this->installEntitySchema('entity_test_mulrev');
+ $this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
@@ -373,8 +376,9 @@ class WorkspaceIntegrationTest extends KernelTestBase {
public function testEntityQueryRelationship() {
$this->initializeWorkspaceModule();
- // Add an entity reference field that targets 'entity_test_mulrev' entities.
- $this->createEntityReferenceField('node', 'page', 'field_test_entity', 'Test entity reference', 'entity_test_mulrev');
+ // Add an entity reference field that targets 'entity_test_mulrevpub'
+ // entities.
+ $this->createEntityReferenceField('node', 'page', 'field_test_entity', 'Test entity reference', 'entity_test_mulrevpub');
// Add an entity reference field that targets 'node' entities so we can test
// references to the same base tables.
@@ -384,8 +388,8 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$node_1 = $this->createNode([
'title' => 'live node 1',
]);
- $entity_test = EntityTestMulRev::create([
- 'name' => 'live entity_test_mulrev',
+ $entity_test = EntityTestMulRevPub::create([
+ 'name' => 'live entity_test_mulrevpub',
'non_rev_field' => 'live non-revisionable value',
]);
$entity_test->save();
@@ -405,7 +409,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$node_2->title->value = 'stage node 2';
$node_2->save();
- $entity_test->name->value = 'stage entity_test_mulrev';
+ $entity_test->name->value = 'stage entity_test_mulrevpub';
$entity_test->non_rev_field->value = 'stage non-revisionable value';
$entity_test->save();
@@ -435,11 +439,15 @@ class WorkspaceIntegrationTest extends KernelTestBase {
->condition('field_test_node.entity.uuid', $node_1->uuid());
// Add conditions for a reference to a different entity type.
+ // @todo Re-enable the two conditions below when we find a way to not join
+ // the workspace_association table for every duplicate entity base table
+ // join.
+ // @see https://www.drupal.org/project/drupal/issues/2983639
$query
// Check a condition on the revision data table.
- ->condition('field_test_entity.entity.name', 'stage entity_test_mulrev')
+ // ->condition('field_test_entity.entity.name', 'stage entity_test_mulrevpub')
// Check a condition on the data table.
- ->condition('field_test_entity.entity.non_rev_field', 'stage non-revisionable value')
+ // ->condition('field_test_entity.entity.non_rev_field', 'stage non-revisionable value')
// Check a condition on the base table.
->condition('field_test_entity.entity.uuid', $entity_test->uuid());
@@ -448,6 +456,41 @@ class WorkspaceIntegrationTest extends KernelTestBase {
}
/**
+ * Tests CRUD operations for unsupported entity types.
+ */
+ public function testDisallowedEntityCRUDInNonDefaultWorkspace() {
+ $this->initializeWorkspaceModule();
+
+ // Create an unsupported entity type in the default workspace.
+ $this->switchToWorkspace('live');
+ $entity_test = EntityTestMulRev::create([
+ 'name' => 'live entity_test_mulrev',
+ ]);
+ $entity_test->save();
+
+ // Switch to a non-default workspace and check that any entity type CRUD are
+ // not allowed.
+ $this->switchToWorkspace('stage');
+
+ // Check updating an existing entity.
+ $entity_test->name->value = 'stage entity_test_mulrev';
+ $entity_test->setNewRevision(TRUE);
+ $this->setExpectedException(EntityStorageException::class, 'This entity can only be saved in the default workspace.');
+ $entity_test->save();
+
+ // Check saving a new entity.
+ $new_entity_test = EntityTestMulRev::create([
+ 'name' => 'stage entity_test_mulrev',
+ ]);
+ $this->setExpectedException(EntityStorageException::class, 'This entity can only be saved in the default workspace.');
+ $new_entity_test->save();
+
+ // Check deleting an existing entity.
+ $this->setExpectedException(EntityStorageException::class, 'This entity can only be deleted in the default workspace.');
+ $entity_test->delete();
+ }
+
+ /**
* Checks entity load, entity queries and views results for a test scenario.
*
* @param array $expected
diff --git a/core/modules/workspace/workspace.module b/core/modules/workspace/workspace.module
index dd2ef38..ced2be4 100644
--- a/core/modules/workspace/workspace.module
+++ b/core/modules/workspace/workspace.module
@@ -8,6 +8,7 @@
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
@@ -17,6 +18,7 @@ use Drupal\views\ViewExecutable;
use Drupal\workspace\EntityAccess;
use Drupal\workspace\EntityOperations;
use Drupal\workspace\EntityTypeInfo;
+use Drupal\workspace\FormOperations;
use Drupal\workspace\ViewsQueryAlter;
/**
@@ -46,8 +48,13 @@ function workspace_entity_type_build(array &$entity_types) {
* Implements hook_form_alter().
*/
function workspace_form_alter(&$form, FormStateInterface $form_state, $form_id) {
- return \Drupal::service('class_resolver')
- ->getInstanceFromDefinition(EntityOperations::class)
+ if ($form_state->getFormObject() instanceof EntityFormInterface) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityFormAlter($form, $form_state, $form_id);
+ }
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(FormOperations::class)
->formAlter($form, $form_state, $form_id);
}
@@ -88,6 +95,15 @@ function workspace_entity_update(EntityInterface $entity) {
}
/**
+ * Implements hook_entity_predelete().
+ */
+function workspace_entity_predelete(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityPredelete($entity);
+}
+
+/**
* Implements hook_entity_access().
*
* @see \Drupal\workspace\EntityAccess