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: ...@@ -283,6 +283,12 @@ block_settings:
view_mode: view_mode:
type: string type: string
label: 'View mode' label: 'View mode'
visibility:
type: sequence
label: 'Visibility Conditions'
sequence:
- type: condition.plugin.[id]
label: 'Visibility Condition'
provider: provider:
type: string type: string
label: 'Provider' label: 'Provider'
...@@ -297,3 +303,11 @@ condition.plugin: ...@@ -297,3 +303,11 @@ condition.plugin:
negate: negate:
type: boolean type: boolean
label: 'Negate' 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 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\Core\Plugin\Context; namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\Context\ContextInterface; use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Component\Plugin\ContextAwarePluginInterface; use Drupal\Component\Plugin\ContextAwarePluginInterface;
use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Exception\ContextException;
...@@ -122,6 +123,12 @@ public function getMatchingContexts(array $contexts, DataDefinitionInterface $de ...@@ -122,6 +123,12 @@ public function getMatchingContexts(array $contexts, DataDefinitionInterface $de
* {@inheritdoc} * {@inheritdoc}
*/ */
public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()) { 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(); $plugin_contexts = $plugin->getContextDefinitions();
// Loop through each context and set it on the plugin if it matches one of // Loop through each context and set it on the plugin if it matches one of
// the contexts expected by the plugin. // the contexts expected by the plugin.
......
...@@ -11,22 +11,6 @@ ...@@ -11,22 +11,6 @@
use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request; 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(). * Implements hook_help().
*/ */
...@@ -398,10 +382,11 @@ function template_preprocess_block(&$variables) { ...@@ -398,10 +382,11 @@ function template_preprocess_block(&$variables) {
*/ */
function block_user_role_delete($role) { function block_user_role_delete($role) {
foreach (entity_load_multiple('block') as $block) { foreach (entity_load_multiple('block') as $block) {
$visibility = $block->get('visibility'); /** @var $block \Drupal\block\BlockInterface */
if (isset($visibility['roles']['roles'][$role->id()])) { $visibility = $block->getVisibility();
unset($visibility['roles']['roles'][$role->id()]); if (isset($visibility['user_role']['roles'][$role->id()])) {
$block->set('visibility', $visibility); unset($visibility['user_role']['roles'][$role->id()]);
$block->getPlugin()->setVisibilityConfig('user_role', $visibility['user_role']);
$block->save(); $block->save();
} }
} }
...@@ -428,10 +413,11 @@ function block_menu_delete(Menu $menu) { ...@@ -428,10 +413,11 @@ function block_menu_delete(Menu $menu) {
function block_language_entity_delete(Language $language) { function block_language_entity_delete(Language $language) {
// Remove the block visibility settings for the deleted language. // Remove the block visibility settings for the deleted language.
foreach (entity_load_multiple('block') as $block) { 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()])) { if (isset($visibility['language']['langcodes'][$language->id()])) {
unset($visibility['language']['langcodes'][$language->id()]); unset($visibility['language']['langcodes'][$language->id()]);
$block->set('visibility', $visibility); $block->getPlugin()->setVisibilityConfig('language', $visibility['language']);
$block->save(); $block->save();
} }
} }
......
...@@ -6,3 +6,18 @@ services: ...@@ -6,3 +6,18 @@ services:
class: Drupal\block\Theme\AdminDemoNegotiator class: Drupal\block\Theme\AdminDemoNegotiator
tags: tags:
- { name: theme_negotiator, priority: 1000 } - { 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.*: ...@@ -19,40 +19,6 @@ block.block.*:
provider: provider:
type: string type: string
label: 'Provider' 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: plugin:
type: string type: string
label: 'Plugin' label: 'Plugin'
......
...@@ -26,10 +26,10 @@ ...@@ -26,10 +26,10 @@
return vals.join(', '); 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) { $('#edit-settings-visibility-request-path').drupalSetSummary(function (context) {
var $pages = $(context).find('textarea[name="visibility[path][pages]"]'); var $pages = $(context).find('textarea[name="settings[visibility][request_path][pages]"]');
if (!$pages.val()) { if (!$pages.val()) {
return Drupal.t('Not restricted'); return Drupal.t('Not restricted');
} }
......
...@@ -8,53 +8,19 @@ ...@@ -8,53 +8,19 @@
namespace Drupal\block; namespace Drupal\block;
use Drupal\Core\Entity\EntityAccessController; use Drupal\Core\Entity\EntityAccessController;
use Drupal\Core\Entity\EntityControllerInterface;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface; 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. * Provides a Block access controller.
*/ */
class BlockAccessController extends EntityAccessController implements EntityControllerInterface { class BlockAccessController extends EntityAccessController {
/**
* 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')
);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) { protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
/** @var $entity \Drupal\block\BlockInterface */
if ($operation != 'view') { if ($operation != 'view') {
return parent::checkAccess($entity, $operation, $langcode, $account); return parent::checkAccess($entity, $operation, $langcode, $account);
} }
...@@ -64,65 +30,8 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A ...@@ -64,65 +30,8 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
return FALSE; return FALSE;
} }
// User role access handling. // Delegate to the plugin.
// If a block has no roles associated, it is displayed for every role. return $entity->getPlugin()->access($account);
// 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;
} }
} }
...@@ -7,6 +7,11 @@ ...@@ -7,6 +7,11 @@
namespace Drupal\block; 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\Core\Plugin\ContextAwarePluginBase;
use Drupal\block\BlockInterface; use Drupal\block\BlockInterface;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
...@@ -27,6 +32,22 @@ ...@@ -27,6 +32,22 @@
*/ */
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface { 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} * {@inheritdoc}
*/ */
...@@ -54,7 +75,9 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition ...@@ -54,7 +75,9 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getConfiguration() { public function getConfiguration() {
return $this->configuration; return array(
'visibility' => $this->getVisibilityConditions()->getConfiguration(),
) + $this->configuration;
} }
/** /**
...@@ -75,6 +98,11 @@ public function setConfiguration(array $configuration) { ...@@ -75,6 +98,11 @@ public function setConfiguration(array $configuration) {
* An associative array with the default configuration. * An associative array with the default configuration.
*/ */
protected function baseConfigurationDefaults() { 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( return array(
'id' => $this->getPluginId(), 'id' => $this->getPluginId(),
'label' => '', 'label' => '',
...@@ -84,6 +112,7 @@ protected function baseConfigurationDefaults() { ...@@ -84,6 +112,7 @@ protected function baseConfigurationDefaults() {
'max_age' => 0, 'max_age' => 0,
'contexts' => array(), 'contexts' => array(),
), ),
'visibility' => $visibility,
); );
} }
...@@ -112,10 +141,38 @@ public function calculateDependencies() { ...@@ -112,10 +141,38 @@ public function calculateDependencies() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function access(AccountInterface $account) { 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); 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. * Indicates whether the block should be shown.
* *
...@@ -212,6 +269,53 @@ public function buildConfigurationForm(array $form, array &$form_state) { ...@@ -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['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'];