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() {
$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 @@
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;
}
<?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:
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: { }
......
<?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 @@
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;
}
}
<?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 @@
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 @@
/**
* 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.
......@@ -48,6 +49,28 @@ public function setInfo($info);
*/
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.
*
......
......@@ -28,4 +28,19 @@ public function buildRow(EntityInterface $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() {
$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.'),
......
......@@ -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 @@
*/
class BlockContent extends EditorialContentEntityBase implements BlockContentInterface {
use RefinableDependentAccessTrait;
/**
* The theme the block is being created in.
*
......@@ -118,15 +121,24 @@ public function getTheme() {
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($this->isReusable() || (isset($this->original) && $this->original->isReusable())) {
static::invalidateBlockPluginCache();
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
/** @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) {
->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;
}
......@@ -282,6 +302,27 @@ public function setRevisionLogMessage($revision_log_message) {
return $this;
}
/**
* {@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.
*/
......
<?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) {