Commit 1228438e authored by webchick's avatar webchick

Issue #2278541 by tim.plunkett, kim.pepper, Bojhan, EclipseGc: Refactor block...

Issue #2278541 by tim.plunkett, kim.pepper, Bojhan, EclipseGc: Refactor block visibility to use condition plugins.
parent 4a13614a
......@@ -283,6 +283,12 @@ block_settings:
view_mode:
type: string
label: 'View mode'
visibility:
type: sequence
label: 'Visibility Conditions'
sequence:
- type: condition.plugin.[id]
label: 'Visibility Condition'
provider:
type: string
label: 'Provider'
......@@ -297,3 +303,11 @@ condition.plugin:
negate:
type: boolean
label: 'Negate'
uuid:
type: string
label: 'UUID'
context_mapping:
type: sequence
label: 'Context assignments'
sequence:
- type: string
<?php
/**
* @file
* Contains \Drupal\Core\Condition\ConditionAccessResolverTrait.
*/
namespace Drupal\Core\Condition;
use Drupal\Component\Plugin\Exception\PluginException;
/**
* Resolves a set of conditions.
*/
trait ConditionAccessResolverTrait {
/**
* Resolves the given conditions based on the condition logic ('and'/'or').
*
* @param \Drupal\Core\Condition\ConditionInterface[] $conditions
* A set of conditions.
* @param string $condition_logic
* The logic used to compute access, either 'and' or 'or'.
*
* @return bool
* Whether these conditions grant or deny access.
*/
protected function resolveConditions($conditions, $condition_logic) {
foreach ($conditions as $condition) {
try {
$pass = $condition->execute();
}
catch (PluginException $e) {
// If a condition is missing context, consider that a fail.
$pass = FALSE;
}
// If a condition fails and all conditions were needed, deny access.
if (!$pass && $condition_logic == 'and') {
return FALSE;
}
// If a condition passes and only one condition was needed, grant access.
elseif ($pass && $condition_logic == 'or') {
return TRUE;
}
}
// Return TRUE if logic was 'and', meaning all rules passed.
// Return FALSE if logic was 'or', meaning no rule passed.
return $condition_logic == 'and';
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Condition\ConditionPluginBag.
*/
namespace Drupal\Core\Condition;
use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Core\Plugin\DefaultPluginBag;
/**
* Provides a collection of condition plugins.
*/
class ConditionPluginBag extends DefaultPluginBag {
/**
* An array of collected contexts for conditions.
*
* @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/
protected $conditionContexts = array();
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Condition\ConditionInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
$configuration = parent::getConfiguration();
// Remove configuration if it matches the defaults.
foreach ($configuration as $instance_id => $instance_config) {
$default_config = array();
$default_config['id'] = $instance_id;
$default_config += $this->get($instance_id)->defaultConfiguration();
if ($default_config === $instance_config) {
unset($configuration[$instance_id]);
}
}
return $configuration;
}
/**
* Sets the condition context for a given name.
*
* @param string $name
* The name of the context.
* @param \Drupal\Component\Plugin\Context\ContextInterface $context
* The context to add.
*
* @return $this
*/
public function addContext($name, ContextInterface $context) {
$this->conditionContexts[$name] = $context;
return $this;
}
/**
* Gets the values for all defined contexts.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
public function getConditionContexts() {
return $this->conditionContexts;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Component\Plugin\ContextAwarePluginInterface;
use Drupal\Component\Plugin\Exception\ContextException;
......@@ -122,6 +123,12 @@ public function getMatchingContexts(array $contexts, DataDefinitionInterface $de
* {@inheritdoc}
*/
public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()) {
if ($plugin instanceof ConfigurablePluginInterface) {
$configuration = $plugin->getConfiguration();
if (isset($configuration['context_mapping'])) {
$mappings += array_flip($configuration['context_mapping']);
}
}
$plugin_contexts = $plugin->getContextDefinitions();
// Loop through each context and set it on the plugin if it matches one of
// the contexts expected by the plugin.
......
......@@ -11,22 +11,6 @@
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Shows this block on every page except the listed pages.
*/
const BLOCK_VISIBILITY_NOTLISTED = 0;
/**
* Shows this block on only the listed pages.
*/
const BLOCK_VISIBILITY_LISTED = 1;
/**
* Shows this block if the associated PHP code returns TRUE.
*/
const BLOCK_VISIBILITY_PHP = 2;
/**
* Implements hook_help().
*/
......@@ -398,10 +382,11 @@ function template_preprocess_block(&$variables) {
*/
function block_user_role_delete($role) {
foreach (entity_load_multiple('block') as $block) {
$visibility = $block->get('visibility');
if (isset($visibility['roles']['roles'][$role->id()])) {
unset($visibility['roles']['roles'][$role->id()]);
$block->set('visibility', $visibility);
/** @var $block \Drupal\block\BlockInterface */
$visibility = $block->getVisibility();
if (isset($visibility['user_role']['roles'][$role->id()])) {
unset($visibility['user_role']['roles'][$role->id()]);
$block->getPlugin()->setVisibilityConfig('user_role', $visibility['user_role']);
$block->save();
}
}
......@@ -428,10 +413,11 @@ function block_menu_delete(Menu $menu) {
function block_language_entity_delete(Language $language) {
// Remove the block visibility settings for the deleted language.
foreach (entity_load_multiple('block') as $block) {
$visibility = $block->get('visibility');
/** @var $block \Drupal\block\BlockInterface */
$visibility = $block->getVisibility();
if (isset($visibility['language']['langcodes'][$language->id()])) {
unset($visibility['language']['langcodes'][$language->id()]);
$block->set('visibility', $visibility);
$block->getPlugin()->setVisibilityConfig('language', $visibility['language']);
$block->save();
}
}
......
......@@ -6,3 +6,18 @@ services:
class: Drupal\block\Theme\AdminDemoNegotiator
tags:
- { name: theme_negotiator, priority: 1000 }
block.current_user_context:
class: Drupal\block\EventSubscriber\CurrentUserContext
arguments: ['@current_user', '@entity.manager']
tags:
- { name: 'event_subscriber' }
block.current_language_context:
class: Drupal\block\EventSubscriber\CurrentLanguageContext
arguments: ['@language_manager']
tags:
- { name: 'event_subscriber' }
block.node_route_context:
class: Drupal\block\EventSubscriber\NodeRouteContext
arguments: ['@request_stack']
tags:
- { name: 'event_subscriber' }
......@@ -19,40 +19,6 @@ block.block.*:
provider:
type: string
label: 'Provider'
visibility:
type: mapping
label: 'Visibility settings'
mapping:
path:
type: mapping
label: 'Pages'
mapping:
visibility:
type: integer
label: 'Visibility'
pages:
type: string
label: 'Show block on specific pages'
role:
type: mapping
label: 'Roles'
mapping:
roles:
type: sequence
label: 'Show block for specific roles'
sequence:
- type: string
label: 'Role'
node_type:
type: mapping
label: 'Content types'
mapping:
types:
type: sequence
label: 'Show block for specific content types'
sequence:
- type: string
label: 'Node type'
plugin:
type: string
label: 'Plugin'
......
......@@ -26,10 +26,10 @@
return vals.join(', ');
}
$('#edit-visibility-node-type, #edit-visibility-language, #edit-visibility-role').drupalSetSummary(checkboxesSummary);
$('#edit-settings-visibility-node-type, #edit-settings-visibility-language, #edit-settings-visibility-user-role').drupalSetSummary(checkboxesSummary);
$('#edit-visibility-path').drupalSetSummary(function (context) {
var $pages = $(context).find('textarea[name="visibility[path][pages]"]');
$('#edit-settings-visibility-request-path').drupalSetSummary(function (context) {
var $pages = $(context).find('textarea[name="settings[visibility][request_path][pages]"]');
if (!$pages.val()) {
return Drupal.t('Not restricted');
}
......
......@@ -8,53 +8,19 @@
namespace Drupal\block;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\Core\Entity\EntityControllerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Component\Utility\Unicode;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a Block access controller.
*/
class BlockAccessController extends EntityAccessController implements EntityControllerInterface {
/**
* The node grant storage.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a BlockAccessController object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The alias manager.
*/
public function __construct(EntityTypeInterface $entity_type, AliasManagerInterface $alias_manager) {
parent::__construct($entity_type);
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('path.alias_manager')
);
}
class BlockAccessController extends EntityAccessController {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
/** @var $entity \Drupal\block\BlockInterface */
if ($operation != 'view') {
return parent::checkAccess($entity, $operation, $langcode, $account);
}
......@@ -64,65 +30,8 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
return FALSE;
}
// User role access handling.
// If a block has no roles associated, it is displayed for every role.
// For blocks with roles associated, if none of the user's roles matches
// the settings from this block, access is denied.
$visibility = $entity->get('visibility');
if (!empty($visibility['role']['roles']) && !array_intersect(array_filter($visibility['role']['roles']), $account->getRoles())) {
// No match.
return FALSE;
}
// Page path handling.
// Limited visibility blocks must list at least one page.
if (!empty($visibility['path']['visibility']) && $visibility['path']['visibility'] == BLOCK_VISIBILITY_LISTED && empty($visibility['path']['pages'])) {
return FALSE;
}
// Match path if necessary.
if (!empty($visibility['path']['pages'])) {
// Assume there are no matches until one is found.
$page_match = FALSE;
// Convert path to lowercase. This allows comparison of the same path
// with different case. Ex: /Page, /page, /PAGE.
$pages = drupal_strtolower($visibility['path']['pages']);
if ($visibility['path']['visibility'] < BLOCK_VISIBILITY_PHP) {
// Compare the lowercase path alias (if any) and internal path.
$path = current_path();
$path_alias = Unicode::strtolower($this->aliasManager->getAliasByPath($path));
$page_match = drupal_match_path($path_alias, $pages) || (($path != $path_alias) && drupal_match_path($path, $pages));
// When $block->visibility has a value of 0
// (BLOCK_VISIBILITY_NOTLISTED), the block is displayed on all pages
// except those listed in $block->pages. When set to 1
// (BLOCK_VISIBILITY_LISTED), it is displayed only on those pages
// listed in $block->pages.
$page_match = !($visibility['path']['visibility'] xor $page_match);
}
// If there are page visibility restrictions and this page does not
// match, deny access.
if (!$page_match) {
return FALSE;
}
}
// Language visibility settings.
if (!empty($visibility['language']['langcodes']) && array_filter($visibility['language']['langcodes'])) {
if (empty($visibility['language']['langcodes'][\Drupal::languageManager()->getCurrentLanguage($visibility['language']['language_type'])->id])) {
return FALSE;
}
}
// If the plugin denies access, then deny access. Apply plugin access checks
// last, because it's almost certainly cheaper to first apply Block's own
// visibility checks.
if (!$entity->getPlugin()->access($account)) {
return FALSE;
}
return TRUE;
// Delegate to the plugin.
return $entity->getPlugin()->access($account);
}
}
......@@ -7,6 +7,11 @@
namespace Drupal\block;
use Drupal\block\Event\BlockConditionContextEvent;
use Drupal\block\Event\BlockEvents;
use Drupal\Component\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Condition\ConditionAccessResolverTrait;
use Drupal\Core\Condition\ConditionPluginBag;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\block\BlockInterface;
use Drupal\Component\Utility\Unicode;
......@@ -27,6 +32,22 @@
*/
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface {
use ConditionAccessResolverTrait;
/**
* The condition plugin bag.
*
* @var \Drupal\Core\Condition\ConditionPluginBag
*/
protected $conditionBag;
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Executable\ExecutableManagerInterface
*/
protected $conditionPluginManager;
/**
* {@inheritdoc}
*/
......@@ -54,7 +75,9 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
return array(
'visibility' => $this->getVisibilityConditions()->getConfiguration(),
) + $this->configuration;
}
/**
......@@ -75,6 +98,11 @@ public function setConfiguration(array $configuration) {
* An associative array with the default configuration.
*/
protected function baseConfigurationDefaults() {
// @todo Allow list of conditions to be configured in
// https://drupal.org/node/2284687.
$visibility = array_map(function ($definition) {
return array('id' => $definition['id']);
}, $this->conditionPluginManager()->getDefinitions());
return array(
'id' => $this->getPluginId(),
'label' => '',
......@@ -84,6 +112,7 @@ protected function baseConfigurationDefaults() {
'max_age' => 0,
'contexts' => array(),
),
'visibility' => $visibility,
);
}
......@@ -112,10 +141,38 @@ public function calculateDependencies() {
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
// @todo Move block visibility here in https://drupal.org/node/2278541.
// @todo Add in a context mapping until the UI supports configuring them,
// see https://drupal.org/node/2284687.
$mappings['user_role']['current_user'] = 'user';
$conditions = $this->getVisibilityConditions();
$contexts = $this->getConditionContexts();
foreach ($conditions as $condition_id => $condition) {
if ($condition instanceof ContextAwarePluginInterface) {
if (!isset($mappings[$condition_id])) {
$mappings[$condition_id] = array();
}
$this->contextHandler()->applyContextMapping($condition, $contexts, $mappings[$condition_id]);
}
}
if ($this->resolveConditions($conditions, 'and', $contexts, $mappings) === FALSE) {
return FALSE;
}
return $this->blockAccess($account);
}
/**
* Gets the values for all defined contexts.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
protected function getConditionContexts() {
$conditions = $this->getVisibilityConditions();
$this->eventDispatcher()->dispatch(BlockEvents::CONDITION_CONTEXT, new BlockConditionContextEvent($conditions));
return $conditions->getConditionContexts();
}
/**
* Indicates whether the block should be shown.
*
......@@ -212,6 +269,53 @@ public function buildConfigurationForm(array $form, array &$form_state) {
$form['cache']['contexts']['#description'] .= ' ' . t('This block is <em>always</em> varied by the following contexts: %required-context-list.', array('%required-context-list' => $required_context_list));
}
$form['visibility_tabs'] = array(
'#type' => 'vertical_tabs',
'#title' => $this->t('Visibility'),
'#parents' => array('visibility_tabs'),
'#attached' => array(
'library' => array(
'block/drupal.block',
),
),
);
foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
$condition_form = $condition->buildConfigurationForm(array(), $form_state);
$condition_form['#type'] = 'details';
$condition_form['#title'] = $condition->getPluginDefinition()['label'];
$condition_form['#group'] = 'visibility_tabs';
$form['visibility'][$condition_id] = $condition_form;
}
// @todo Determine if there is a better way to rename the conditions.
if (isset($form['visibility']['node_type'])) {
$form['visibility']['node_type']['#title'] = $this->t('Content types');
$form['visibility']['node_type']['bundles']['#title'] = $this->t('Content types');
$form['visibility']['node_type']['negate']['#type'] = 'value';
$form['visibility']['node_type']['negate']['#title_display'] = 'invisible';
$form['visibility']['node_type']['negate']['#value'] = $form['visibility']['node_type']['negate']['#default_value'];
}
if (isset($form['visibility']['user_role'])) {
$form['visibility']['user_role']['#title'] = $this->t('Roles');
unset($form['visibility']['user_role']['roles']['#description']);
$form['visibility']['user_role']['negate']['#type'] = 'value';
$form['visibility']['user_role']['negate']['#value'] = $form['visibility']['user_role']['negate']['#default_value'];
}
if (isset($form['visibility']['request_path'])) {
$form['visibility']['request_path']['#title'] = $this->t('Pages');
$form['visibility']['request_path']['negate']['#type'] = 'radios';
$form['visibility']['request_path']['negate']['#title_display'] = 'invisible';
$form['visibility']['request_path']['negate']['#default_value'] = (int) $form['visibility']['request_path']['negate']['#default_value'];
$form['visibility']['request_path']['negate']['#options'] = array(
$this->t('Show for the listed pages'),
$this->t('Hide for the listed pages'),
);
}
if (isset($form['visibility']['language'])) {
$form['visibility']['language']['negate']['#type'] = 'value';
$form['visibility']['language']['negate']['#value'] = $form['visibility']['language']['negate']['#default_value'];
}
// Add plugin-specific settings for this block type.
$form += $this->blockForm($form, $form_state);
return $form;
......@@ -236,6 +340,14 @@ public function validateConfigurationForm(array &$form, array &$form_state) {
// Transform the #type = checkboxes value to a numerically indexed array.
$form_state['values']['cache']['contexts'] = array_values(array_filter($form_state['values']['cache']['contexts']));
foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
// Allow the condition to validate the form.
$condition_values = array(
'values' => &$form_state['values']['visibility'][$condition_id],
);
$condition->validateConfigurationForm($form, $condition_values);
}
$this->blockValidate($form, $form_state);
}
......@@ -259,6 +371,13 @@ public function submitConfigurationForm(array &$form, array &$form_state) {
$this->configuration['label_display'] = $form_state['values']['label_display'];
$this->configuration['provider'] = $form_state['values']['provider'];
$this->configuration['cache'] = $form_state['values']['cache'];
foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
// Allow the condition to submit the form.
$condition_values = array(
'values' => &$form_state['values']['visibility'][$condition_id],
);
$condition->submitConfigurationForm($form, $condition_values);
}
$this->blockSubmit($form, $form_state);
}
}
......@@ -347,4 +466,61 @@ public function isCacheable() {
return $max_age === Cache::PERMANENT || $max_age > 0;
}
/**
* {@inheritdoc}
*/
public function getVisibilityConditions() {
if (!isset($this->conditionBag)) {
$this->conditionBag = new ConditionPluginBag($this->conditionPluginManager(), $this->configuration['visibility']);
}
return $this->conditionBag;
}
/**
* {@inheritdoc}
*/
public function getVisibilityCondition($instance_id) {
return $this->getVisibilityConditions()->get($instance_id);
}
/**
* {@inheritdoc}
*/
public function setVisibilityConfig($instance_id, array $configuration) {
$this->getVisibilityConditions()->setInstanceConfiguration($instance_id, $configuration);
return $this;
}
/**
* Gets the condition plugin manager.
*
* @return \Drupal\Core\Executable\ExecutableManagerInterface
* The condition plugin manager.
*/
protected function conditionPluginManager() {
if (!isset($this->conditionPluginManager)) {
$this->conditionPluginManager = \Drupal::service('plugin.manager.condition');
}
return $this->conditionPluginManager;
}
/**
* Wraps the event dispatcher.
*
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
* The event dispatcher.
*/
protected function eventDispatcher() {
return \Drupal::service('event_dispatcher');
}
/**
* Wraps the context handler.
*
* @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected function contextHandler() {
return \Drupal::service('context.handler');
}
}
......@@ -10,9 +10,6 @@
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -34,24 +31,14 @@ class BlockForm extends EntityForm {
*/
protected $storage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**