Commit 51c89793 authored by effulgentsia's avatar effulgentsia

Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett, phenaproxima,...

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 98430f12)
parent 25c15a1e
...@@ -138,3 +138,19 @@ function block_content_update_8400() { ...@@ -138,3 +138,19 @@ function block_content_update_8400() {
$definition_update_manager->uninstallFieldStorageDefinition($content_translation_status); $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);
}
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig; 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(). * Implements hook_help().
...@@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') { ...@@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
return $field; 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;
}
<?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;
});
}
...@@ -431,6 +431,44 @@ display: ...@@ -431,6 +431,44 @@ display:
entity_type: block_content entity_type: block_content
entity_field: type entity_field: type
plugin_id: bundle 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: { } sorts: { }
title: 'Custom block library' title: 'Custom block library'
header: { } header: { }
......
<?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;
}
}
<?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();
}
<?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);
}
<?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;
}
}
...@@ -2,27 +2,85 @@ ...@@ -2,27 +2,85 @@
namespace Drupal\block_content; namespace Drupal\block_content;
use Drupal\block_content\Access\DependentAccessInterface;
use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface; 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. * Defines the access control handler for the custom block entity type.
* *
* @see \Drupal\block_content\Entity\BlockContent * @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} * {@inheritdoc}
*/ */
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view') { if ($operation === 'view') {
return AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity) $access = AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity)
->orIf(AccessResult::allowedIfHasPermission($account, 'administer blocks')); ->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;
} }
} }
<?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';
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace Drupal\block_content; namespace Drupal\block_content;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedInterface;
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
/** /**
* Provides an interface defining a custom block entity. * 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. * Returns the block revision log message.
...@@ -48,6 +49,28 @@ public function setInfo($info); ...@@ -48,6 +49,28 @@ public function setInfo($info);
*/ */
public function setRevisionLog($revision_log); 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. * Sets the theme value.
* *
......
...@@ -28,4 +28,19 @@ public function buildRow(EntityInterface $entity) { ...@@ -28,4 +28,19 @@ public function buildRow(EntityInterface $entity) {
return $row + parent::buildRow($entity); 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();
}
} }
...@@ -23,6 +23,8 @@ public function getViewsData() { ...@@ -23,6 +23,8 @@ public function getViewsData() {
$data['block_content_field_data']['type']['field']['id'] = 'field'; $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'] = [ $data['block_content']['block_content_listing_empty'] = [
'title' => $this->t('Empty block library behavior'), 'title' => $this->t('Empty block library behavior'),
'help' => $this->t('Provides a link to add a new block.'), 'help' => $this->t('Provides a link to add a new block.'),
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace Drupal\block_content\Entity; namespace Drupal\block_content\Entity;
use Drupal\block_content\Access\RefinableDependentAccessTrait;
use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
...@@ -77,6 +78,8 @@ ...@@ -77,6 +78,8 @@
*/ */
class BlockContent extends EditorialContentEntityBase implements BlockContentInterface { class BlockContent extends EditorialContentEntityBase implements BlockContentInterface {
use RefinableDependentAccessTrait;
/** /**
* The theme the block is being created in. * The theme the block is being created in.
* *
...@@ -118,7 +121,9 @@ public function getTheme() { ...@@ -118,7 +121,9 @@ public function getTheme() {
*/ */
public function postSave(EntityStorageInterface $storage, $update = TRUE) { public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update); parent::postSave($storage, $update);
static::invalidateBlockPluginCache(); if ($this->isReusable() || (isset($this->original) && $this->original->isReusable())) {
static::invalidateBlockPluginCache();
}
} }
/** /**
...@@ -126,7 +131,14 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { ...@@ -126,7 +131,14 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
*/ */
public static function postDelete(EntityStorageInterface $storage, array $entities) { public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $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 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ...@@ -200,6 +212,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setTranslatable(TRUE) ->setTranslatable(TRUE)
->setRevisionable(TRUE); ->setRevisionable(TRUE);
$fields['reusable'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Reusable'))
->setDescription(t('A boolean indicating whether this block is reusabl