summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreffulgentsia2018-07-16 22:43:44 (GMT)
committereffulgentsia2018-07-26 01:41:28 (GMT)
commit51c89793e7da11ce716529046760dbbef0e09f7a (patch)
treeceb1c74425cde2100f42bf868ea73902e0b37a6a
parent25c15a1e76744fb0849112f381331e32dc84aa9b (diff)
Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett, phenaproxima, larowlan, samuel.mortenson, Berdir, EclipseGc, johnzzon: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used
(cherry picked from commit 98430f1244e0c97826c9346299742bbbe1e0ac1a)
-rw-r--r--core/modules/block_content/block_content.install16
-rw-r--r--core/modules/block_content/block_content.module73
-rw-r--r--core/modules/block_content/block_content.post_update.php46
-rw-r--r--core/modules/block_content/config/optional/views.view.block_content.yml38
-rw-r--r--core/modules/block_content/src/Access/AccessGroupAnd.php50
-rw-r--r--core/modules/block_content/src/Access/DependentAccessInterface.php35
-rw-r--r--core/modules/block_content/src/Access/RefinableDependentAccessInterface.php48
-rw-r--r--core/modules/block_content/src/Access/RefinableDependentAccessTrait.php52
-rw-r--r--core/modules/block_content/src/BlockContentAccessControlHandler.php64
-rw-r--r--core/modules/block_content/src/BlockContentEvents.php31
-rw-r--r--core/modules/block_content/src/BlockContentInterface.php25
-rw-r--r--core/modules/block_content/src/BlockContentListBuilder.php15
-rw-r--r--core/modules/block_content/src/BlockContentViewsData.php2
-rw-r--r--core/modules/block_content/src/Entity/BlockContent.php45
-rw-r--r--core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php70
-rw-r--r--core/modules/block_content/src/Plugin/Derivative/BlockContent.php2
-rw-r--r--core/modules/block_content/src/Plugin/views/wizard/BlockContent.php35
-rw-r--r--core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php80
-rw-r--r--core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml9
-rw-r--r--core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml602
-rw-r--r--core/modules/block_content/tests/src/Functional/BlockContentListTest.php15
-rw-r--r--core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php15
-rw-r--r--core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php5
-rw-r--r--core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php155
-rw-r--r--core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php52
-rw-r--r--core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php300
-rw-r--r--core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php66
-rw-r--r--core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php189
-rw-r--r--core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php55
-rw-r--r--core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php36
-rw-r--r--core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php160
31 files changed, 2379 insertions, 7 deletions
diff --git a/core/modules/block_content/block_content.install b/core/modules/block_content/block_content.install
index e3da6bd..fab15b4 100644
--- a/core/modules/block_content/block_content.install
+++ b/core/modules/block_content/block_content.install
@@ -138,3 +138,19 @@ function block_content_update_8400() {
$definition_update_manager->uninstallFieldStorageDefinition($content_translation_status);
}
}
+
+/**
+ * Add 'reusable' field to 'block_content' entities.
+ */
+function block_content_update_8600() {
+ $reusable = BaseFieldDefinition::create('boolean')
+ ->setLabel(t('Reusable'))
+ ->setDescription(t('A boolean indicating whether this block is reusable.'))
+ ->setTranslatable(FALSE)
+ ->setRevisionable(FALSE)
+ ->setDefaultValue(TRUE)
+ ->setInitialValue(TRUE);
+
+ \Drupal::entityDefinitionUpdateManager()
+ ->installFieldStorageDefinition('reusable', 'block_content', 'block_content', $reusable);
+}
diff --git a/core/modules/block_content/block_content.module b/core/modules/block_content/block_content.module
index 3adc979..98f0925 100644
--- a/core/modules/block_content/block_content.module
+++ b/core/modules/block_content/block_content.module
@@ -8,6 +8,9 @@
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Database\Query\ConditionInterface;
/**
* Implements hook_help().
@@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
return $field;
}
+
+/**
+ * Implements hook_query_TAG_alter().
+ *
+ * Alters any 'entity_reference' query where the entity type is
+ * 'block_content' and the query has the tag 'block_content_access'.
+ *
+ * These queries should only return reusable blocks unless a condition on
+ * 'reusable' is explicitly set.
+ *
+ * Block_content entities that are reusable should by default not be selectable
+ * as entity reference values. A module can still create an instance of
+ * \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
+ * that will allow selection of non-reusable blocks by explicitly setting
+ * a condition on the 'reusable' field.
+ *
+ * @see \Drupal\block_content\BlockContentAccessControlHandler
+ */
+function block_content_query_entity_reference_alter(AlterableInterface $query) {
+ if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) {
+ $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
+ if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) {
+ $query->condition("$data_table.reusable", TRUE);
+ }
+ }
+}
+
+/**
+ * Utility function to find nested conditions using the reusable field.
+ *
+ * @todo Replace this function with a call to the API in
+ * https://www.drupal.org/project/drupal/issues/2984930
+ *
+ * @param array $condition
+ * The condition or condition group to check.
+ * @param array $tables
+ * The tables from the related select query.
+ *
+ * @see \Drupal\Core\Database\Query\SelectInterface::getTables
+ *
+ * @return bool
+ * Whether the conditions contain any condition using the reusable field.
+ */
+function _block_content_has_reusable_condition(array $condition, array $tables) {
+ // If this is a condition group call this function recursively for each nested
+ // condition until a condition is found that return TRUE.
+ if (isset($condition['#conjunction'])) {
+ foreach (array_filter($condition, 'is_array') as $nested_condition) {
+ if (_block_content_has_reusable_condition($nested_condition, $tables)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+ if (isset($condition['field'])) {
+ $field = $condition['field'];
+ if (is_object($field) && $field instanceof ConditionInterface) {
+ return _block_content_has_reusable_condition($field->conditions(), $tables);
+ }
+ $field_parts = explode('.', $field);
+ $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
+ foreach ($tables as $table) {
+ if ($table['table'] === $data_table && $field_parts[0] === $table['alias'] && $field_parts[1] === 'reusable') {
+ return TRUE;
+ }
+ }
+
+ }
+ return FALSE;
+}
diff --git a/core/modules/block_content/block_content.post_update.php b/core/modules/block_content/block_content.post_update.php
new file mode 100644
index 0000000..6587658
--- /dev/null
+++ b/core/modules/block_content/block_content.post_update.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for Custom Block.
+ */
+
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+
+/**
+ * Adds a 'reusable' filter to all Custom Block views.
+ */
+function block_content_post_update_add_views_reusable_filter(&$sandbox = NULL) {
+ $data_table = \Drupal::entityTypeManager()
+ ->getDefinition('block_content')
+ ->getDataTable();
+
+ \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($data_table) {
+ /** @var \Drupal\views\ViewEntityInterface $view */
+ if ($view->get('base_table') != $data_table) {
+ return FALSE;
+ }
+ $save_view = FALSE;
+ $displays = $view->get('display');
+ foreach ($displays as $display_name => &$display) {
+ // Update the default display and displays that have overridden filters.
+ if (!isset($display['display_options']['filters']['reusable']) &&
+ ($display_name === 'default' || isset($display['display_options']['filters']))) {
+ $display['display_options']['filters']['reusable'] = [
+ 'id' => 'reusable',
+ 'plugin_id' => 'boolean',
+ 'table' => $data_table,
+ 'field' => 'reusable',
+ 'value' => '1',
+ 'entity_type' => 'block_content',
+ 'entity_field' => 'reusable',
+ ];
+ $save_view = TRUE;
+ }
+ }
+ if ($save_view) {
+ $view->set('display', $displays);
+ }
+ return $save_view;
+ });
+}
diff --git a/core/modules/block_content/config/optional/views.view.block_content.yml b/core/modules/block_content/config/optional/views.view.block_content.yml
index 1be5a04..2c00886 100644
--- a/core/modules/block_content/config/optional/views.view.block_content.yml
+++ b/core/modules/block_content/config/optional/views.view.block_content.yml
@@ -431,6 +431,44 @@ display:
entity_type: block_content
entity_field: type
plugin_id: bundle
+ reusable:
+ id: reusable
+ table: block_content_field_data
+ field: reusable
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: '1'
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: block_content
+ entity_field: reusable
+ plugin_id: boolean
sorts: { }
title: 'Custom block library'
header: { }
diff --git a/core/modules/block_content/src/Access/AccessGroupAnd.php b/core/modules/block_content/src/Access/AccessGroupAnd.php
new file mode 100644
index 0000000..7bb62bc
--- /dev/null
+++ b/core/modules/block_content/src/Access/AccessGroupAnd.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\block_content\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * An access group where all the dependencies must be allowed.
+ *
+ * @internal
+ */
+class AccessGroupAnd implements AccessibleInterface {
+
+
+ /**
+ * The access dependencies.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface[]
+ */
+ protected $dependencies = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addDependency(AccessibleInterface $dependency) {
+ $this->dependencies[] = $dependency;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ $access_result = AccessResult::neutral();
+ foreach (array_slice($this->dependencies, 1) as $dependency) {
+ $access_result = $access_result->andIf($dependency->access($operation, $account, TRUE));
+ }
+ return $return_as_object ? $access_result : $access_result->isAllowed();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDependencies() {
+ return $this->dependencies;
+ }
+
+}
diff --git a/core/modules/block_content/src/Access/DependentAccessInterface.php b/core/modules/block_content/src/Access/DependentAccessInterface.php
new file mode 100644
index 0000000..bc6a6dc
--- /dev/null
+++ b/core/modules/block_content/src/Access/DependentAccessInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\block_content\Access;
+
+/**
+ * Interface for AccessibleInterface objects that have an access dependency.
+ *
+ * Objects should implement this interface when their access depends on access
+ * to another object that implements \Drupal\Core\Access\AccessibleInterface.
+ * This interface simply provides the getter method for the access
+ * dependency object. Objects that implement this interface are responsible for
+ * checking access of the access dependency because the dependency may not take
+ * effect in all cases. For instance an entity may only need the access
+ * dependency set when it is embedded within another entity and its access
+ * should be dependent on access to the entity in which it is embedded.
+ *
+ * To check the access to the dependency the object implementing this interface
+ * can use code like this:
+ * @code
+ * $accessible->getAccessDependency()->access($op, $account, TRUE);
+ * @endcode
+ *
+ * @internal
+ */
+interface DependentAccessInterface {
+
+ /**
+ * Gets the access dependency.
+ *
+ * @return \Drupal\Core\Access\AccessibleInterface|null
+ * The access dependency or NULL if none has been set.
+ */
+ public function getAccessDependency();
+
+}
diff --git a/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php b/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php
new file mode 100644
index 0000000..469de52
--- /dev/null
+++ b/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\block_content\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+
+/**
+ * An interface to allow adding an access dependency.
+ *
+ * @internal
+ */
+interface RefinableDependentAccessInterface extends DependentAccessInterface {
+
+ /**
+ * Sets the access dependency.
+ *
+ * If an access dependency is already set this will replace the existing
+ * dependency.
+ *
+ * @param \Drupal\Core\Access\AccessibleInterface $access_dependency
+ * The object upon which access depends.
+ *
+ * @return $this
+ */
+ public function setAccessDependency(AccessibleInterface $access_dependency);
+
+ /**
+ * Adds an access dependency into the existing access dependency.
+ *
+ * If no existing dependency is currently set this will set the dependency
+ * will be set to the new value.
+ *
+ * If there is an existing dependency and it is not an instance of
+ * AccessGroupAnd the dependency will be set as a new AccessGroupAnd
+ * instance with the existing and new dependencies as the members of the
+ * group.
+ *
+ * If there is an existing dependency and it is a instance of AccessGroupAnd
+ * the dependency will be added to the existing access group.
+ *
+ * @param \Drupal\Core\Access\AccessibleInterface $access_dependency
+ * The access dependency to merge.
+ *
+ * @return $this
+ */
+ public function addAccessDependency(AccessibleInterface $access_dependency);
+
+}
diff --git a/core/modules/block_content/src/Access/RefinableDependentAccessTrait.php b/core/modules/block_content/src/Access/RefinableDependentAccessTrait.php
new file mode 100644
index 0000000..98b2a54
--- /dev/null
+++ b/core/modules/block_content/src/Access/RefinableDependentAccessTrait.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\block_content\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+
+/**
+ * Trait for \Drupal\block_content\Access\RefinableDependentAccessInterface.
+ *
+ * @internal
+ */
+trait RefinableDependentAccessTrait {
+
+ /**
+ * The access dependency.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $accessDependency;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAccessDependency(AccessibleInterface $access_dependency) {
+ $this->accessDependency = $access_dependency;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAccessDependency() {
+ return $this->accessDependency;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addAccessDependency(AccessibleInterface $access_dependency) {
+ if (empty($this->accessDependency)) {
+ $this->accessDependency = $access_dependency;
+ return $this;
+ }
+ if (!$this->accessDependency instanceof AccessGroupAnd) {
+ $accessGroup = new AccessGroupAnd();
+ $this->accessDependency = $accessGroup->addDependency($this->accessDependency);
+ }
+ $this->accessDependency->addDependency($access_dependency);
+ return $this;
+ }
+
+}
diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php
index 7079ef4..17e61dc 100644
--- a/core/modules/block_content/src/BlockContentAccessControlHandler.php
+++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
@@ -2,27 +2,85 @@
namespace Drupal\block_content;
+use Drupal\block_content\Access\DependentAccessInterface;
+use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines the access control handler for the custom block entity type.
*
* @see \Drupal\block_content\Entity\BlockContent
*/
-class BlockContentAccessControlHandler extends EntityAccessControlHandler {
+class BlockContentAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
+
+ /**
+ * The event dispatcher.
+ *
+ * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ */
+ protected $eventDispatcher;
+
+ /**
+ * BlockContentAccessControlHandler constructor.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type.
+ * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
+ * The event dispatcher.
+ */
+ public function __construct(EntityTypeInterface $entity_type, EventDispatcherInterface $dispatcher) {
+ parent::__construct($entity_type);
+ $this->eventDispatcher = $dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+ return new static(
+ $entity_type,
+ $container->get('event_dispatcher')
+ );
+ }
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view') {
- return AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity)
+ $access = AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity)
->orIf(AccessResult::allowedIfHasPermission($account, 'administer blocks'));
}
- return parent::checkAccess($entity, $operation, $account);
+ else {
+ $access = parent::checkAccess($entity, $operation, $account);
+ }
+ $access->addCacheableDependency($entity);
+ /** @var \Drupal\block_content\BlockContentInterface $entity */
+ if ($entity->isReusable() === FALSE) {
+ if (!$entity instanceof DependentAccessInterface) {
+ throw new \LogicException("Non-reusable block entities must implement \Drupal\block_content\Access\DependentAccessInterface for access control.");
+ }
+ $dependency = $entity->getAccessDependency();
+ if (empty($dependency)) {
+ // If an access dependency has not been set let modules set one.
+ $event = new BlockContentGetDependencyEvent($entity);
+ $this->eventDispatcher->dispatch(BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY, $event);
+ $dependency = $event->getAccessDependency();
+ if (empty($dependency)) {
+ return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control.");
+ }
+ }
+ /** @var \Drupal\Core\Entity\EntityInterface $dependency */
+ $access = $access->andIf($dependency->access($operation, $account, TRUE));
+ }
+ return $access;
}
}
diff --git a/core/modules/block_content/src/BlockContentEvents.php b/core/modules/block_content/src/BlockContentEvents.php
new file mode 100644
index 0000000..85931b7
--- /dev/null
+++ b/core/modules/block_content/src/BlockContentEvents.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\block_content;
+
+/**
+ * Defines events for the block_content module.
+ *
+ * @see \Drupal\block_content\Event\BlockContentGetDependencyEvent
+ *
+ * @internal
+ */
+final class BlockContentEvents {
+
+ /**
+ * Name of the event when getting the dependency of a non-reusable block.
+ *
+ * This event allows modules to provide a dependency for non-reusable block
+ * access if
+ * \Drupal\block_content\Access\DependentAccessInterface::getAccessDependency()
+ * did not return a dependency during access checking.
+ *
+ * @Event
+ *
+ * @see \Drupal\block_content\Event\BlockContentGetDependencyEvent
+ * @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
+ *
+ * @var string
+ */
+ const BLOCK_CONTENT_GET_DEPENDENCY = 'block_content.get_dependency';
+
+}
diff --git a/core/modules/block_content/src/BlockContentInterface.php b/core/modules/block_content/src/BlockContentInterface.php
index 75fdc59..bba6e46 100644
--- a/core/modules/block_content/src/BlockContentInterface.php
+++ b/core/modules/block_content/src/BlockContentInterface.php
@@ -2,6 +2,7 @@
namespace Drupal\block_content;
+use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
@@ -10,7 +11,7 @@ use Drupal\Core\Entity\RevisionLogInterface;
/**
* Provides an interface defining a custom block entity.
*/
-interface BlockContentInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityPublishedInterface {
+interface BlockContentInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityPublishedInterface, RefinableDependentAccessInterface {
/**
* Returns the block revision log message.
@@ -49,6 +50,28 @@ interface BlockContentInterface extends ContentEntityInterface, EntityChangedInt
public function setRevisionLog($revision_log);
/**
+ * Determines if the block is reusable or not.
+ *
+ * @return bool
+ * Returns TRUE if reusable and FALSE otherwise.
+ */
+ public function isReusable();
+
+ /**
+ * Sets the block to be reusable.
+ *
+ * @return $this
+ */
+ public function setReusable();
+
+ /**
+ * Sets the block to be non-reusable.
+ *
+ * @return $this
+ */
+ public function setNonReusable();
+
+ /**
* Sets the theme value.
*
* When creating a new block content block from the block library, the user is
diff --git a/core/modules/block_content/src/BlockContentListBuilder.php b/core/modules/block_content/src/BlockContentListBuilder.php
index 7a4bdfc..f254a76 100644
--- a/core/modules/block_content/src/BlockContentListBuilder.php
+++ b/core/modules/block_content/src/BlockContentListBuilder.php
@@ -28,4 +28,19 @@ class BlockContentListBuilder extends EntityListBuilder {
return $row + parent::buildRow($entity);
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEntityIds() {
+ $query = $this->getStorage()->getQuery()
+ ->sort($this->entityType->getKey('id'));
+ $query->condition('reusable', TRUE);
+
+ // Only add the pager if a limit is specified.
+ if ($this->limit) {
+ $query->pager($this->limit);
+ }
+ return $query->execute();
+ }
+
}
diff --git a/core/modules/block_content/src/BlockContentViewsData.php b/core/modules/block_content/src/BlockContentViewsData.php
index 010ede0..e9ff0eb 100644
--- a/core/modules/block_content/src/BlockContentViewsData.php
+++ b/core/modules/block_content/src/BlockContentViewsData.php
@@ -23,6 +23,8 @@ class BlockContentViewsData extends EntityViewsData {
$data['block_content_field_data']['type']['field']['id'] = 'field';
+ $data['block_content_field_data']['table']['wizard_id'] = 'block_content';
+
$data['block_content']['block_content_listing_empty'] = [
'title' => $this->t('Empty block library behavior'),
'help' => $this->t('Provides a link to add a new block.'),
diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php
index 7696da0..b9bc8a9 100644
--- a/core/modules/block_content/src/Entity/BlockContent.php
+++ b/core/modules/block_content/src/Entity/BlockContent.php
@@ -2,6 +2,7 @@
namespace Drupal\block_content\Entity;
+use Drupal\block_content\Access\RefinableDependentAccessTrait;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
@@ -77,6 +78,8 @@ use Drupal\user\UserInterface;
*/
class BlockContent extends EditorialContentEntityBase implements BlockContentInterface {
+ use RefinableDependentAccessTrait;
+
/**
* The theme the block is being created in.
*
@@ -118,7 +121,9 @@ class BlockContent extends EditorialContentEntityBase implements BlockContentInt
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
- static::invalidateBlockPluginCache();
+ if ($this->isReusable() || (isset($this->original) && $this->original->isReusable())) {
+ static::invalidateBlockPluginCache();
+ }
}
/**
@@ -126,7 +131,14 @@ class BlockContent extends EditorialContentEntityBase implements BlockContentInt
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
- static::invalidateBlockPluginCache();
+ /** @var \Drupal\block_content\BlockContentInterface $block */
+ foreach ($entities as $block) {
+ if ($block->isReusable()) {
+ // If any deleted blocks are reusable clear the block cache.
+ static::invalidateBlockPluginCache();
+ return;
+ }
+ }
}
/**
@@ -200,6 +212,14 @@ class BlockContent extends EditorialContentEntityBase implements BlockContentInt
->setTranslatable(TRUE)
->setRevisionable(TRUE);
+ $fields['reusable'] = BaseFieldDefinition::create('boolean')
+ ->setLabel(t('Reusable'))
+ ->setDescription(t('A boolean indicating whether this block is reusable.'))
+ ->setTranslatable(FALSE)
+ ->setRevisionable(FALSE)
+ ->setDefaultValue(TRUE)
+ ->setInitialValue(TRUE);
+
return $fields;
}
@@ -283,6 +303,27 @@ class BlockContent extends EditorialContentEntityBase implements BlockContentInt
}
/**
+ * {@inheritdoc}
+ */
+ public function isReusable() {
+ return $this->get('reusable')->first()->get('value')->getCastedValue();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setReusable() {
+ return $this->set('reusable', TRUE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setNonReusable() {
+ return $this->set('reusable', FALSE);
+ }
+
+ /**
* Invalidates the block plugin cache after changes and deletions.
*/
protected static function invalidateBlockPluginCache() {
diff --git a/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php
new file mode 100644
index 0000000..e705172
--- /dev/null
+++ b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\block_content\Event;
+
+use Drupal\block_content\BlockContentInterface;
+use Drupal\Core\Access\AccessibleInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Block content event to allow setting an access dependency.
+ *
+ * @internal
+ */
+class BlockContentGetDependencyEvent extends Event {
+
+ /**
+ * The block content entity.
+ *
+ * @var \Drupal\block_content\BlockContentInterface
+ */
+ protected $blockContent;
+
+ /**
+ * The dependency.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $accessDependency;
+
+ /**
+ * BlockContentGetDependencyEvent constructor.
+ *
+ * @param \Drupal\block_content\BlockContentInterface $blockContent
+ * The block content entity.
+ */
+ public function __construct(BlockContentInterface $blockContent) {
+ $this->blockContent = $blockContent;
+ }
+
+ /**
+ * Gets the block content entity.
+ *
+ * @return \Drupal\block_content\BlockContentInterface
+ * The block content entity.
+ */
+ public function getBlockContentEntity() {
+ return $this->blockContent;
+ }
+
+ /**
+ * Gets the access dependency.
+ *
+ * @return \Drupal\Core\Access\AccessibleInterface
+ * The access dependency.
+ */
+ public function getAccessDependency() {
+ return $this->accessDependency;
+ }
+
+ /**
+ * Sets the access dependency.
+ *
+ * @param \Drupal\Core\Access\AccessibleInterface $access_dependency
+ * The access dependency.
+ */
+ public function setAccessDependency(AccessibleInterface $access_dependency) {
+ $this->accessDependency = $access_dependency;
+ }
+
+}
diff --git a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php
index ac82a6c..ba1ab98 100644
--- a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php
+++ b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php
@@ -43,7 +43,7 @@ class BlockContent extends DeriverBase implements ContainerDeriverInterface {
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
- $block_contents = $this->blockContentStorage->loadMultiple();
+ $block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]);
// Reset the discovered definitions.
$this->derivatives = [];
/** @var $block_content \Drupal\block_content\Entity\BlockContent */
diff --git a/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php b/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php
new file mode 100644
index 0000000..6072501
--- /dev/null
+++ b/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\block_content\Plugin\views\wizard;
+
+use Drupal\views\Plugin\views\wizard\WizardPluginBase;
+
+/**
+ * Used for creating 'block_content' views with the wizard.
+ *
+ * @ViewsWizard(
+ * id = "block_content",
+ * base_table = "block_content_field_data",
+ * title = @Translation("Custom Block"),
+ * )
+ */
+class BlockContent extends WizardPluginBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters() {
+ $filters = parent::getFilters();
+ $filters['reusable'] = [
+ 'id' => 'reusable',
+ 'plugin_id' => 'boolean',
+ 'table' => $this->base_table,
+ 'field' => 'reusable',
+ 'value' => '1',
+ 'entity_type' => $this->entityTypeId,
+ 'entity_field' => 'reusable',
+ ];
+ return $filters;
+ }
+
+}
diff --git a/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php b/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php
new file mode 100644
index 0000000..1a46c48
--- /dev/null
+++ b/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\block_content_test\Plugin\EntityReferenceSelection;
+
+use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
+
+/**
+ * Test EntityReferenceSelection with conditions on the 'reusable' field.
+ */
+class TestSelection extends DefaultSelection {
+
+ /**
+ * The condition type.
+ *
+ * @var string
+ */
+ protected $conditionType;
+
+ /**
+ * Whether to set the condition for reusable or non-reusable blocks.
+ *
+ * @var bool
+ */
+ protected $isReusable;
+
+ /**
+ * Sets the test mode.
+ *
+ * @param string $condition_type
+ * The condition type.
+ * @param bool $is_reusable
+ * Whether to set the condition for reusable or non-reusable blocks.
+ */
+ public function setTestMode($condition_type = NULL, $is_reusable = NULL) {
+ $this->conditionType = $condition_type;
+ $this->isReusable = $is_reusable;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
+ $query = parent::buildEntityQuery($match, $match_operator);
+ if ($this->conditionType) {
+ /** @var \Drupal\Core\Database\Query\ConditionInterface $add_condition */
+ $add_condition = NULL;
+ switch ($this->conditionType) {
+ case 'base':
+ $add_condition = $query;
+ break;
+
+ case 'group':
+ $group = $query->andConditionGroup()
+ ->exists('type');
+ $add_condition = $group;
+ $query->condition($group);
+ break;
+
+ case "nested_group":
+ $query->exists('type');
+ $sub_group = $query->andConditionGroup()
+ ->exists('type');
+ $add_condition = $sub_group;
+ $group = $query->andConditionGroup()
+ ->exists('type')
+ ->condition($sub_group);
+ $query->condition($group);
+ break;
+ }
+ if ($this->isReusable) {
+ $add_condition->condition('reusable', 1);
+ }
+ else {
+ $add_condition->condition('reusable', 0);
+ }
+ }
+ return $query;
+ }
+
+}
diff --git a/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml b/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml
new file mode 100644
index 0000000..3ca2d1b
--- /dev/null
+++ b/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml
@@ -0,0 +1,9 @@
+name: "Custom Block module reusable tests"
+type: module
+description: "Support module for custom block reusable testing."
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+ - drupal:block_content
+ - drupal:views
diff --git a/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml
new file mode 100644
index 0000000..3c622a9
--- /dev/null
+++ b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml
@@ -0,0 +1,602 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - block_content
+ - user
+id: block_content
+label: 'Custom block library'
+module: views
+description: 'Find and manage custom blocks.'
+tag: default
+base_table: block_content_field_data
+base_field: id
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'administer blocks'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 50
+ offset: 0
+ id: 0
+ total_pages: null
+ tags:
+ previous: '‹ Previous'
+ next: 'Next ›'
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ style:
+ type: table
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ override: true
+ sticky: false
+ caption: ''
+ summary: ''
+ description: ''
+ columns:
+ info: info
+ type: type
+ changed: changed
+ operations: operations
+ info:
+ info:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ type:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ changed:
+ sortable: true
+ default_sort_order: desc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ operations:
+ sortable: false
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ default: changed
+ empty_table: true
+ row:
+ type: fields
+ fields:
+ info:
+ id: info
+ table: block_content_field_data
+ field: info
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Block description'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ settings:
+ link_to_entity: true
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: null
+ entity_field: info
+ plugin_id: field
+ type:
+ id: type
+ table: block_content_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Block type'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_label
+ settings:
+ link: false
+ group_column: target_id
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: block_content
+ entity_field: type
+ plugin_id: field
+ changed:
+ id: changed
+ table: block_content_field_data
+ field: changed
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Updated
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ entity_type: block_content
+ entity_field: changed
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ plugin_id: field
+ operations:
+ id: operations
+ table: block_content
+ field: operations
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Operations
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ destination: true
+ entity_type: block_content
+ plugin_id: entity_operations
+ filters:
+ info:
+ id: info
+ table: block_content_field_data
+ field: info
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: contains
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: info_op
+ label: 'Block description'
+ description: ''
+ use_operator: false
+ operator: info_op
+ identifier: info
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: block_content
+ entity_field: info
+ plugin_id: string
+ type:
+ id: type
+ table: block_content_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: type_op
+ label: 'Block type'
+ description: ''
+ use_operator: false
+ operator: type_op
+ identifier: type
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: block_content
+ entity_field: type
+ plugin_id: bundle
+ sorts: { }
+ title: 'Custom block library'
+ header: { }
+ footer: { }
+ empty:
+ area_text_custom:
+ id: area_text_custom
+ table: views
+ field: area_text_custom
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ tokenize: false
+ content: 'There are no custom blocks available.'
+ plugin_id: text_custom
+ block_content_listing_empty:
+ admin_label: ''
+ empty: true
+ field: block_content_listing_empty
+ group_type: group
+ id: block_content_listing_empty
+ label: ''
+ relationship: none
+ table: block_content
+ plugin_id: block_content_listing_empty
+ entity_type: block_content
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ max-age: 0
+ tags: { }
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: admin/structure/block/block-content
+ menu:
+ type: tab
+ title: 'Custom block library'
+ description: ''
+ parent: block.admin_display
+ weight: 0
+ context: '0'
+ menu_name: admin
+ cache_metadata:
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ max-age: 0
+ tags: { }
+ page_2:
+ display_plugin: page
+ id: page_2
+ display_title: 'Page 2'
+ position: 2
+ display_options:
+ display_extenders: { }
+ path: extra-view-display
+ filters:
+ type:
+ id: type
+ table: block_content_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: type_op
+ label: 'Block type'
+ description: ''
+ use_operator: false
+ operator: type_op
+ identifier: type
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: block_content
+ entity_field: type
+ plugin_id: bundle
+ info:
+ id: info
+ table: block_content_field_data
+ field: info
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'contains'
+ value: block2
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: block_content
+ entity_field: info
+ plugin_id: string
+ defaults:
+ filters: false
+ filter_groups: false
+ filter_groups:
+ operator: AND
+ groups:
+ 1: AND
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ tags: { }
diff --git a/core/modules/block_content/tests/src/Functional/BlockContentListTest.php b/core/modules/block_content/tests/src/Functional/BlockContentListTest.php
index 9a26f1c..8919c05 100644
--- a/core/modules/block_content/tests/src/Functional/BlockContentListTest.php
+++ b/core/modules/block_content/tests/src/Functional/BlockContentListTest.php
@@ -2,6 +2,8 @@
namespace Drupal\Tests\block_content\Functional;
+use Drupal\block_content\Entity\BlockContent;
+
/**
* Tests the listing of custom blocks.
*
@@ -104,6 +106,19 @@ class BlockContentListTest extends BlockContentTestBase {
// Confirm that the empty text is displayed.
$this->assertText(t('There are no custom blocks yet.'));
+
+ $block_content = BlockContent::create([
+ 'info' => 'Non-reusable block',
+ 'type' => 'basic',
+ 'reusable' => FALSE,
+ ]);
+ $block_content->save();
+
+ $this->drupalGet('admin/structure/block/block-content');
+ // Confirm that the empty text is displayed.
+ $this->assertSession()->pageTextContains('There are no custom blocks yet.');
+ // Confirm the non-reusable block is not on the page.
+ $this->assertSession()->pageTextNotContains('Non-reusable block');
}
}
diff --git a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
index 1c623be..f9cf29e 100644
--- a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
+++ b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
@@ -2,6 +2,8 @@
namespace Drupal\Tests\block_content\Functional;
+use Drupal\block_content\Entity\BlockContent;
+
/**
* Tests the Views-powered listing of custom blocks.
*
@@ -112,6 +114,19 @@ class BlockContentListViewsTest extends BlockContentTestBase {
// Confirm that the empty text is displayed.
$this->assertText('There are no custom blocks available.');
$this->assertLink('custom block');
+
+ $block_content = BlockContent::create([
+ 'info' => 'Non-reusable block',
+ 'type' => 'basic',
+ 'reusable' => FALSE,
+ ]);
+ $block_content->save();
+
+ $this->drupalGet('admin/structure/block/block-content');
+ // Confirm that the empty text is displayed.
+ $this->assertSession()->pageTextContains('There are no custom blocks available.');
+ // Confirm the non-reusable block is not on the page.
+ $this->assertSession()->pageTextNotContains('Non-reusable block');
}
}
diff --git a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
index c77585e..4a3ac11 100644
--- a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
+++ b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
@@ -92,6 +92,11 @@ abstract class BlockContentResourceTestBase extends EntityResourceTestBase {
'value' => 'en',
],
],
+ 'reusable' => [
+ [
+ 'value' => TRUE,
+ ],
+ ],
'type' => [
[
'target_id' => 'basic',
diff --git a/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php
new file mode 100644
index 0000000..4e8d993
--- /dev/null
+++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\Tests\block_content\Functional\Update;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests 'reusable' field related update functions for the Block Content module.
+ *
+ * @group Update
+ * @group legacy
+ */
+class BlockContentReusableUpdateTest extends UpdatePathTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setDatabaseDumpFiles() {
+ $this->databaseDumpFiles = [
+ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
+ ];
+ }
+
+ /**
+ * Tests adding 'reusable' entity base field to the block content entity type.
+ *
+ * @see block_content_update_8600
+ * @see block_content_post_update_add_views_reusable_filter
+ */
+ public function testReusableFieldAddition() {
+ $assert_session = $this->assertSession();
+ $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+
+ // Delete custom block library view.
+ $this->config('views.view.block_content')->delete();
+ // Install the test module with the 'block_content' view with an extra
+ // display with overridden filters. This extra display should also have a
+ // filter added for 'reusable' field so that it does not expose non-reusable
+ // fields. This display also has a filter that only shows blocks that
+ // contain 'block2' in the 'info' field.
+ $this->container->get('module_installer')->install(['block_content_view_override']);
+
+ // Ensure that 'reusable' field is not present before updates.
+ $this->assertEmpty($entity_definition_update_manager->getFieldStorageDefinition('reusable', 'block_content'));
+
+ // Ensure that 'reusable' filter is not present before updates.
+ $view_config = \Drupal::configFactory()->get('views.view.block_content');
+ $this->assertFalse($view_config->isNew());
+ $this->assertEmpty($view_config->get('display.default.display_options.filters.reusable'));
+ $this->assertEmpty($view_config->get('display.page_2.display_options.filters.reusable'));
+ // Run updates.
+ $this->runUpdates();
+
+ // Ensure that 'reusable' filter is present after updates.
+ \Drupal::configFactory()->clearStaticCache();
+ $view_config = \Drupal::configFactory()->get('views.view.block_content');
+ $this->assertNotEmpty($view_config->get('display.default.display_options.filters.reusable'));
+ $this->assertNotEmpty($view_config->get('display.page_2.display_options.filters.reusable'));
+
+ // Check that the field exists and is configured correctly.
+ $reusable_field = $entity_definition_update_manager->getFieldStorageDefinition('reusable', 'block_content');
+ $this->assertEquals('Reusable', $reusable_field->getLabel());
+ $this->assertEquals('A boolean indicating whether this block is reusable.', $reusable_field->getDescription());
+ $this->assertEquals(FALSE, $reusable_field->isRevisionable());
+ $this->assertEquals(FALSE, $reusable_field->isTranslatable());
+
+ $after_block1 = BlockContent::create([
+ 'info' => 'After update block1',
+ 'type' => 'basic_block',
+ ]);
+ $after_block1->save();
+ // Add second block that will be shown with the 'info' filter on the
+ // additional view display.
+ $after_block2 = BlockContent::create([
+ 'info' => 'After update block2',
+ 'type' => 'basic_block',
+ ]);
+ $after_block2->save();
+
+ $this->assertTrue($after_block1->isReusable());
+ $this->assertTrue($after_block2->isReusable());
+
+ $admin_user = $this->drupalCreateUser(['administer blocks']);
+ $this->drupalLogin($admin_user);
+
+ $block_non_reusable = BlockContent::create([
+ 'info' => 'block1 non reusable',
+ 'type' => 'basic_block',
+ 'reusable' => FALSE,
+ ]);
+ $block_non_reusable->save();
+ // Add second block that would be shown with the 'info' filter on the
+ // additional view display if the 'reusable' filter was not added.
+ $block2_non_reusable = BlockContent::create([
+ 'info' => 'block2 non reusable',
+ 'type' => 'basic_block',
+ 'reusable' => FALSE,
+ ]);
+ $block2_non_reusable->save();
+ $this->assertFalse($block_non_reusable->isReusable());
+ $this->assertFalse($block2_non_reusable->isReusable());
+
+ // Ensure the Custom Block view shows the reusable blocks only.
+ $this->drupalGet('admin/structure/block/block-content');
+ $assert_session->statusCodeEquals('200');
+ $assert_session->responseContains('view-id-block_content');
+ $assert_session->pageTextContains($after_block1->label());
+ $assert_session->pageTextContains($after_block2->label());
+ $assert_session->pageTextNotContains($block_non_reusable->label());
+ $assert_session->pageTextNotContains($block2_non_reusable->label());
+
+ // Ensure the view's other display also only shows reusable blocks and still
+ // filters on the 'info' field.
+ $this->drupalGet('extra-view-display');
+ $assert_session->statusCodeEquals('200');
+ $assert_session->responseContains('view-id-block_content');
+ $assert_session->pageTextNotContains($after_block1->label());
+ $assert_session->pageTextContains($after_block2->label());
+ $assert_session->pageTextNotContains($block_non_reusable->label());
+ $assert_session->pageTextNotContains($block2_non_reusable->label());
+
+ // Ensure the Custom Block listing without Views installed shows the only
+ // reusable blocks.
+ $this->drupalGet('admin/structure/block/block-content');
+ $this->container->get('module_installer')->uninstall(['views_ui', 'views']);
+ $this->drupalGet('admin/structure/block/block-content');
+ $assert_session->statusCodeEquals('200');
+ $assert_session->responseNotContains('view-id-block_content');
+ $assert_session->pageTextContains($after_block1->label());
+ $assert_session->pageTextContains($after_block2->label());
+ $assert_session->pageTextNotContains($block_non_reusable->label());
+ $assert_session->pageTextNotContains($block2_non_reusable->label());
+
+ $this->drupalGet('block/' . $after_block1->id());
+ $assert_session->statusCodeEquals('200');
+
+ // Ensure the non-reusable block is not accessible in the form.
+ $this->drupalGet('block/' . $block_non_reusable->id());
+ $assert_session->statusCodeEquals('403');
+
+ $this->drupalLogout();
+
+ $this->drupalLogin($this->createUser([
+ 'access user profiles',
+ 'administer blocks',
+ ]));
+ $this->drupalGet('block/' . $after_block1->id());
+ $assert_session->statusCodeEquals('200');
+
+ $this->drupalGet('block/' . $block_non_reusable->id());
+ $assert_session->statusCodeEquals('403');
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php b/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php
new file mode 100644
index 0000000..2c59e5c
--- /dev/null
+++ b/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\Tests\block_content\Functional\Views;
+
+use Drupal\Tests\block_content\Functional\BlockContentTestBase;
+
+/**
+ * Tests block_content wizard and generic entity integration.
+ *
+ * @group block_content
+ */
+class BlockContentWizardTest extends BlockContentTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['block_content', 'views_ui'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->drupalLogin($this->drupalCreateUser(['administer views']));
+ $this->createBlockContentType('Basic block');
+ }
+
+ /**
+ * Tests creating a 'block_content' entity view.
+ */
+ public function testViewAddBlockContent() {
+ $view = [];
+ $view['label'] = $this->randomMachineName(16);
+ $view['id'] = strtolower($this->randomMachineName(16));
+ $view['description'] = $this->randomMachineName(16);
+ $view['page[create]'] = FALSE;
+ $view['show[wizard_key]'] = 'block_content';
+ $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
+
+ $view_storage_controller = $this->container->get('entity_type.manager')->getStorage('view');
+ /** @var \Drupal\views\Entity\View $view */
+ $view = $view_storage_controller->load($view['id']);
+
+ $display_options = $view->getDisplay('default')['display_options'];
+
+ $this->assertEquals('block_content', $display_options['filters']['reusable']['entity_type']);
+ $this->assertEquals('reusable', $display_options['filters']['reusable']['entity_field']);
+ $this->assertEquals('boolean', $display_options['filters']['reusable']['plugin_id']);
+ $this->assertEquals('1', $display_options['filters']['reusable']['value']);
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php
new file mode 100644
index 0000000..64b524d
--- /dev/null
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php
@@ -0,0 +1,300 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel;
+
+use Drupal\block_content\BlockContentAccessControlHandler;
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+
+/**
+ * Tests the block content entity access handler.
+ *
+ * @coversDefaultClass \Drupal\block_content\BlockContentAccessControlHandler
+ *
+ * @group block_content
+ */
+class BlockContentAccessHandlerTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'block',
+ 'block_content',
+ 'system',
+ 'user',
+ ];
+
+ /**
+ * The BlockContent access controller to test.
+ *
+ * @var \Drupal\block_content\BlockContentAccessControlHandler
+ */
+ protected $accessControlHandler;
+
+ /**
+ * The BlockContent entity used for testing.
+ *
+ * @var \Drupal\block_content\Entity\BlockContent
+ */
+ protected $blockEntity;
+
+ /**
+ * The test role.
+ *
+ * @var \Drupal\user\RoleInterface
+ */
+ protected $role;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->installSchema('system', ['sequence']);
+ $this->installSchema('system', ['sequences']);
+ $this->installSchema('user', ['users_data']);
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('block_content');
+
+ // Create a block content type.
+ $block_content_type = BlockContentType::create([
+ 'id' => 'square',
+ 'label' => 'A square block type',
+ 'description' => "Provides a block type that is square.",
+ ]);
+ $block_content_type->save();
+
+ $this->blockEntity = BlockContent::create([
+ 'info' => 'The Block',
+ 'type' => 'square',
+ ]);
+ $this->blockEntity->save();
+
+ // Create user 1 test does not have all permissions.
+ User::create([
+ 'name' => 'admin',
+ ])->save();
+
+ $this->role = Role::create([
+ 'id' => 'roly',
+ 'label' => 'roly poly',
+ ]);
+ $this->role->save();
+ $this->accessControlHandler = new BlockContentAccessControlHandler(\Drupal::entityTypeManager()->getDefinition('block_content'), \Drupal::service('event_dispatcher'));
+ }
+
+ /**
+ * @covers ::checkAccess
+ *
+ * @dataProvider providerTestAccess
+ */
+ public function testAccess($operation, $published, $reusable, $permissions, $parent_access, $expected_access) {
+ $published ? $this->blockEntity->setPublished() : $this->blockEntity->setUnpublished();
+ $reusable ? $this->blockEntity->setReusable() : $this->blockEntity->setNonReusable();
+
+ $user = User::create([
+ 'name' => 'Someone',
+ 'mail' => 'hi@example.com',
+ ]);
+
+ if ($permissions) {
+ foreach ($permissions as $permission) {
+ $this->role->grantPermission($permission);
+ }
+ $this->role->save();
+ }
+ $user->addRole($this->role->id());
+ $user->save();
+
+ if ($parent_access) {
+ $parent_entity = $this->prophesize(AccessibleInterface::class);
+ $expected_parent_result = NULL;
+ switch ($parent_access) {
+ case 'allowed':
+ $expected_parent_result = AccessResult::allowed();
+ break;
+
+ case 'neutral':
+ $expected_parent_result = AccessResult::neutral();
+ break;
+
+ case 'forbidden':
+ $expected_parent_result = AccessResult::forbidden();
+ break;
+ }
+ $parent_entity->access($operation, $user, TRUE)
+ ->willReturn($expected_parent_result)
+ ->shouldBeCalled();
+
+ $this->blockEntity->setAccessDependency($parent_entity->reveal());
+
+ }
+ $this->blockEntity->save();
+
+ $result = $this->accessControlHandler->access($this->blockEntity, $operation, $user, TRUE);
+ switch ($expected_access) {
+ case 'allowed':
+ $this->assertTrue($result->isAllowed());
+ break;
+
+ case 'forbidden':
+ $this->assertTrue($result->isForbidden());
+ break;
+
+ case 'neutral':
+ $this->assertTrue($result->isNeutral());
+ break;
+
+ default:
+ $this->fail('Unexpected access type');
+ }
+ }
+
+ /**
+ * Dataprovider for testAccess().
+ */
+ public function providerTestAccess() {
+ $cases = [
+ 'view:published:reusable' => [
+ 'view',
+ TRUE,
+ TRUE,
+ [],
+ NULL,
+ 'allowed',
+ ],
+ 'view:unpublished:reusable' => [
+ 'view',
+ FALSE,
+ TRUE,
+ [],
+ NULL,
+ 'neutral',
+ ],
+ 'view:unpublished:reusable:admin' => [
+ 'view',
+ FALSE,
+ TRUE,
+ ['administer blocks'],
+ NULL,
+ 'allowed',
+ ],
+ 'view:published:reusable:admin' => [
+ 'view',
+ TRUE,
+ TRUE,
+ ['administer blocks'],
+ NULL,
+ 'allowed',
+ ],
+ 'view:published:non_reusable' => [
+ 'view',
+ TRUE,
+ FALSE,
+ [],
+ NULL,
+ 'forbidden',
+ ],
+ 'view:published:non_reusable:parent_allowed' => [
+ 'view',
+ TRUE,
+ FALSE,
+ [],
+ 'allowed',
+ 'allowed',
+ ],
+ 'view:published:non_reusable:parent_neutral' => [
+ 'view',
+ TRUE,
+ FALSE,
+ [],
+ 'neutral',
+ 'neutral',
+ ],
+ 'view:published:non_reusable:parent_forbidden' => [
+ 'view',
+ TRUE,
+ FALSE,
+ [],
+ 'forbidden',
+ 'forbidden',
+ ],
+ ];
+ foreach (['update', 'delete'] as $operation) {
+ $cases += [
+ $operation . ':published:reusable' => [
+ $operation,
+ TRUE,
+ TRUE,
+ [],
+ NULL,
+ 'neutral',
+ ],
+ $operation . ':unpublished:reusable' => [
+ $operation,
+ FALSE,
+ TRUE,
+ [],
+ NULL,
+ 'neutral',
+ ],
+ $operation . ':unpublished:reusable:admin' => [
+ $operation,
+ FALSE,
+ TRUE,
+ ['administer blocks'],
+ NULL,
+ 'allowed',
+ ],
+ $operation . ':published:reusable:admin' => [
+ $operation,
+ TRUE,
+ TRUE,
+ ['administer blocks'],
+ NULL,
+ 'allowed',
+ ],
+ $operation . ':published:non_reusable' => [
+ $operation,
+ TRUE,
+ FALSE,
+ [],
+ NULL,
+ 'forbidden',
+ ],
+ $operation . ':published:non_reusable:parent_allowed' => [
+ $operation,
+ TRUE,
+ FALSE,
+ [],
+ 'allowed',
+ 'neutral',
+ ],
+ $operation . ':published:non_reusable:parent_neutral' => [
+ $operation,
+ TRUE,
+ FALSE,
+ [],
+ 'neutral',
+ 'neutral',
+ ],
+ $operation . ':published:non_reusable:parent_forbidden' => [
+ $operation,
+ TRUE,
+ FALSE,
+ [],
+ 'forbidden',
+ 'forbidden',
+ ],
+ ];
+ return $cases;
+ }
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php
new file mode 100644
index 0000000..d08d86f
--- /dev/null
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests block content plugin deriver.
+ *
+ * @group block_content
+ */
+class BlockContentDeriverTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['block', 'block_content', 'system', 'user'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+ $this->installSchema('system', ['sequence']);
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('block_content');
+ }
+
+ /**
+ * Tests that only reusable blocks are derived.
+ */
+ public function testReusableBlocksOnlyAreDerived() {
+ // Create a block content type.
+ $block_content_type = BlockContentType::create([
+ 'id' => 'spiffy',
+ 'label' => 'Mucho spiffy',
+ 'description' => "Provides a block type that increases your site's spiffiness by up to 11%",
+ ]);
+ $block_content_type->save();
+ // And a block content entity.
+ $block_content = BlockContent::create([
+ 'info' => 'Spiffy prototype',
+ 'type' => 'spiffy',
+ ]);
+ $block_content->save();
+
+ // Ensure the reusable block content is provided as a derivative block
+ // plugin.
+ /** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */
+ $block_manager = $this->container->get('plugin.manager.block');
+ $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid();
+ $this->assertTrue($block_manager->hasDefinition($plugin_id));
+
+ // Set the block not to be reusable.
+ $block_content->setNonReusable();
+ $block_content->save();
+
+ // Ensure the non-reusable block content is not provided a derivative block
+ // plugin.
+ $this->assertFalse($block_manager->hasDefinition($plugin_id));
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
new file mode 100644
index 0000000..e593336
--- /dev/null
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\block_content_test\Plugin\EntityReferenceSelection\TestSelection;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests EntityReference selection handlers don't return non-reusable blocks.
+ *
+ * @see block_content_query_entity_reference_alter()
+ *
+ * @group block_content
+ */
+class BlockContentEntityReferenceSelectionTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'block',
+ 'block_content',
+ 'block_content_test',
+ 'system',
+ 'user',
+ ];
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Test reusable block.
+ *
+ * @var \Drupal\block_content\BlockContentInterface
+ */
+ protected $blockReusable;
+
+ /**
+ * Test non-reusable block.
+ *
+ * @var \Drupal\block_content\BlockContentInterface
+ */
+ protected $blockNonReusable;
+
+ /**
+ * Test selection handler.
+ *
+ * @var \Drupal\block_content_test\Plugin\EntityReferenceSelection\TestSelection
+ */
+ protected $selectionHandler;
+
+ /**
+ * Test block expectations.
+ *
+ * @var array
+ */
+ protected $expectations;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+ $this->installSchema('system', ['sequence']);
+ $this->installSchema('system', ['sequences']);
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('block_content');
+
+ // Create a block content type.
+ $block_content_type = BlockContentType::create([
+ 'id' => 'spiffy',
+ 'label' => 'Mucho spiffy',
+ 'description' => "Provides a block type that increases your site's spiffiness by up to 11%",
+ ]);
+ $block_content_type->save();
+ $this->entityTypeManager = $this->container->get('entity_type.manager');
+
+ // And reusable block content entities.
+ $this->blockReusable = BlockContent::create([
+ 'info' => 'Reusable Block',
+ 'type' => 'spiffy',
+ ]);
+ $this->blockReusable->save();
+ $this->blockNonReusable = BlockContent::create([
+ 'info' => 'Non-reusable Block',
+ 'type' => 'spiffy',
+ 'reusable' => FALSE,
+ ]);
+ $this->blockNonReusable->save();
+
+ $configuration = [
+ 'target_type' => 'block_content',
+ 'target_bundles' => ['spiffy' => 'spiffy'],
+ 'sort' => ['field' => '_none'],
+ ];
+ $this->selectionHandler = new TestSelection($configuration, '', '', $this->container->get('entity.manager'), $this->container->get('module_handler'), \Drupal::currentUser());
+
+ // Setup the 3 expectation cases.
+ $this->expectations = [
+ 'both_blocks' => [
+ 'spiffy' => [
+ $this->blockReusable->id() => $this->blockReusable->label(),
+ $this->blockNonReusable->id() => $this->blockNonReusable->label(),
+ ],
+ ],
+ 'block_reusable' => ['spiffy' => [$this->blockReusable->id() => $this->blockReusable->label()]],
+ 'block_non_reusable' => ['spiffy' => [$this->blockNonReusable->id() => $this->blockNonReusable->label()]],
+ ];
+ }
+
+ /**
+ * Tests to make sure queries without the expected tags are not altered.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ public function testQueriesNotAltered() {
+ // Ensure that queries without all the tags are not altered.
+ $query = $this->entityTypeManager->getStorage('block_content')->getQuery();
+ $this->assertCount(2, $query->execute());
+
+ $query = $this->entityTypeManager->getStorage('block_content')->getQuery();
+ $query->addTag('block_content_access');
+ $this->assertCount(2, $query->execute());
+
+ $query = $this->entityTypeManager->getStorage('block_content')->getQuery();
+ $query->addTag('entity_query_block_content');
+ $this->assertCount(2, $query->execute());
+ }
+
+ /**
+ * Test with no conditions set.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ public function testNoConditions() {
+ $this->assertEquals(
+ $this->expectations['block_reusable'],
+ $this->selectionHandler->getReferenceableEntities()
+ );
+
+ $this->blockNonReusable->setReusable();
+ $this->blockNonReusable->save();
+
+ // Ensure that the block is now returned as a referenceable entity.
+ $this->assertEquals(
+ $this->expectations['both_blocks'],
+ $this->selectionHandler->getReferenceableEntities()
+ );
+ }
+
+ /**
+ * Tests setting 'reusable' condition on different levels.
+ *
+ * @dataProvider fieldConditionProvider
+ *
+ * @throws \Exception
+ */
+ public function testFieldConditions($condition_type, $is_reusable) {
+ $this->selectionHandler->setTestMode($condition_type, $is_reusable);
+ $this->assertEquals(
+ $is_reusable ? $this->expectations['block_reusable'] : $this->expectations['block_non_reusable'],
+ $this->selectionHandler->getReferenceableEntities()
+ );
+ }
+
+ /**
+ * Provides possible fields and condition types.
+ */
+ public function fieldConditionProvider() {
+ $cases = [];
+ foreach (['base', 'group', 'nested_group'] as $condition_type) {
+ foreach ([TRUE, FALSE] as $reusable) {
+ $cases["$condition_type:" . ($reusable ? 'reusable' : 'non-reusable')] = [
+ $condition_type,
+ $reusable,
+ ];
+ }
+ }
+ return $cases;
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php b/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php
new file mode 100644
index 0000000..8915674
--- /dev/null
+++ b/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\Tests\block_content\Unit\Access;
+
+use Drupal\block_content\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests accessible groups.
+ *
+ * @group block_content
+ */
+class AccessGroupAndTest extends UnitTestCase {
+
+ use AccessibleTestingTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ }
+
+ /**
+ * @covers \Drupal\block_content\Access\AccessGroupAnd
+ */
+ public function testGroups() {
+ $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed());
+ $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden());
+ $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral());
+
+ // Ensure that groups with no dependencies return a neutral access result.
+ $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isNeutral());
+
+ $andNeutral = new AccessGroupAnd();
+ $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible);
+ $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral());
+
+ $andForbidden = $andNeutral;
+ $andForbidden->addDependency($forbiddenAccessible);
+ $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden());
+
+ // Ensure that groups added to other groups works.
+ $andGroupsForbidden = new AccessGroupAnd();
+ $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ // Ensure you can add a non-group accessible object.
+ $andGroupsForbidden->addDependency($allowedAccessible);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php b/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php
new file mode 100644
index 0000000..8aab227
--- /dev/null
+++ b/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\block_content\Unit\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResultInterface;
+
+/**
+ * Helper methods testing accessible interfaces.
+ */
+trait AccessibleTestingTrait {
+
+ /**
+ * The test account.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $account;
+
+ /**
+ * Creates AccessibleInterface object from access result object for testing.
+ *
+ * @param \Drupal\Core\Access\AccessResultInterface $accessResult
+ * The accessible result to return.
+ *
+ * @return \Drupal\Core\Access\AccessibleInterface
+ * The AccessibleInterface object.
+ */
+ private function createAccessibleDouble(AccessResultInterface $accessResult) {
+ $accessible = $this->prophesize(AccessibleInterface::class);
+ $accessible->access('view', $this->account, TRUE)
+ ->willReturn($accessResult);
+ return $accessible->reveal();
+ }
+
+}
diff --git a/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php b/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php
new file mode 100644
index 0000000..2be3686
--- /dev/null
+++ b/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\Tests\block_content\Unit\Access;
+
+use Drupal\block_content\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\block_content\Access\RefinableDependentAccessInterface;
+use Drupal\block_content\Access\RefinableDependentAccessTrait;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\block_content\Access\RefinableDependentAccessTrait
+ *
+ * @group block_content
+ */
+class DependentAccessTest extends UnitTestCase {
+ use AccessibleTestingTrait;
+
+ /**
+ * An accessible object that results in forbidden access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $forbidden;
+
+ /**
+ * An accessible object that results in neutral access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $neutral;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so'));
+ $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion'));
+ }
+
+ /**
+ * Test that the previous dependency is replaced when using set.
+ *
+ * @covers ::setAccessDependency
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testSetAccessDependency($use_set_first) {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ // Calling setAccessDependency() replaces the existing dependency.
+ $testRefinable->setAccessDependency($this->neutral);
+ $dependency = $testRefinable->getAccessDependency();
+ $this->assertFalse($dependency instanceof AccessGroupAnd);
+ $accessResult = $dependency->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResult->getReason());
+ }
+
+ /**
+ * Tests merging a new dependency with existing non-group access dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeNonGroup($use_set_first) {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+ // Ensure the new dependency create a new AND group when merged.
+ $this->assertTrue($dependency instanceof AccessGroupAnd);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+
+ }
+
+ /**
+ * Tests merging a new dependency with an existing access group dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeGroup($use_set_first) {
+ $andGroup = new AccessGroupAnd();
+ $andGroup->addDependency($this->forbidden);
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($andGroup);
+ }
+ else {
+ $testRefinable->addAccessDependency($andGroup);
+ }
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+
+ // Ensure the new dependency is merged with the existing group.
+ $this->assertTrue($dependency instanceof AccessGroupAnd);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+ }
+
+ /**
+ * Dataprovider for all test methods.
+ *
+ * Provides test cases for calling setAccessDependency() or
+ * mergeAccessDependency() first. A call to either should behave the same on a
+ * new RefinableDependentAccessInterface object.
+ */
+ public function providerTestSetFirst() {
+ return [
+ [TRUE],
+ [FALSE],
+ ];
+ }
+
+}
+
+/**
+ * Test class that implements RefinableDependentAccessInterface.
+ */
+class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface {
+
+ use RefinableDependentAccessTrait;
+
+}