Commit 633d8428 authored by alexpott's avatar alexpott

Issue #2339151 by EclipseGc, tim.plunkett, Gábor Hojtsy, effulgentsia:...

Issue #2339151 by EclipseGc, tim.plunkett, Gábor Hojtsy, effulgentsia: Conditions / context system does not allow for multiple configurable contexts, eg. language types
parent cbc449b8
......@@ -300,12 +300,6 @@ 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'
......
......@@ -125,4 +125,28 @@ public function setContextValue($name, $value);
*/
public function validateContexts();
/**
* Returns a mapping of the expected assignment names to their context names.
*
* @return array
* A mapping of the expected assignment names to their context names. For
* example, if one of the $contexts is named 'current_user', but the plugin
* expects a context named 'user', then this map would contain
* 'current_user' => 'user'.
*/
public function getContextMapping();
/**
* Sets a mapping of the expected assignment names to their context names.
*
* @param array $context_mapping
* A mapping of the expected assignment names to their context names. For
* example, if one of the $contexts is named 'current_user', but the plugin
* expects a context named 'user', then this map would contain
* 'current_user' => 'user'.
*
* @return $this
*/
public function setContextMapping(array $context_mapping);
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\StringTranslation\TranslationWrapper;
/**
* @defgroup plugin_context Annotation for context definition
......@@ -94,9 +95,18 @@ public function __construct(array $values) {
$values += array(
'required' => TRUE,
'multiple' => FALSE,
'label' => NULL,
'description' => NULL,
);
// Annotation classes extract data from passed annotation classes directly
// used in the classes they pass to.
foreach (['label', 'description'] as $key) {
// @todo Remove this workaround in https://www.drupal.org/node/2362727.
if (isset($values[$key]) && $values[$key] instanceof TranslationWrapper) {
$values[$key] = (string) $values[$key]->get();
}
else {
$values[$key] = NULL;
}
}
if (isset($values['class']) && !in_array('Drupal\Core\Plugin\Context\ContextDefinitionInterface', class_implements($values['class']))) {
throw new \Exception('ContextDefinition class must implement \Drupal\Core\Plugin\Context\ContextDefinitionInterface.');
}
......
......@@ -8,13 +8,7 @@
namespace Drupal\Core\Block;
use Drupal\block\BlockInterface;
use Drupal\block\Event\BlockConditionContextEvent;
use Drupal\block\Event\BlockEvents;
use Drupal\Component\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Condition\ConditionAccessResolverTrait;
use Drupal\Core\Condition\ConditionPluginCollection;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\Component\Utility\Unicode;
......@@ -35,22 +29,6 @@
*/
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface {
use ConditionAccessResolverTrait;
/**
* The condition plugin collection.
*
* @var \Drupal\Core\Condition\ConditionPluginCollection
*/
protected $conditionCollection;
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Executable\ExecutableManagerInterface
*/
protected $conditionPluginManager;
/**
* The transliteration service.
*
......@@ -84,9 +62,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
* {@inheritdoc}
*/
public function getConfiguration() {
return array(
'visibility' => $this->getVisibilityConditions()->getConfiguration(),
) + $this->configuration;
return $this->configuration;
}
/**
......@@ -107,13 +83,6 @@ 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());
unset($visibility['current_theme']);
return array(
'id' => $this->getPluginId(),
'label' => '',
......@@ -123,7 +92,6 @@ protected function baseConfigurationDefaults() {
'max_age' => 0,
'contexts' => array(),
),
'visibility' => $visibility,
);
}
......@@ -152,25 +120,10 @@ public function calculateDependencies() {
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
// @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]);
}
}
// This should not be hardcoded to an uncacheable access check result, but
// in order to fix that, we need condition plugins to return cache contexts,
// otherwise it will be impossible to determine by which cache contexts the
// result should be varied.
if ($this->resolveConditions($conditions, 'and', $contexts, $mappings) !== FALSE && $this->blockAccess($account)) {
// @todo Remove self::blockAccess() and force individual plugins to return
// their own AccessResult logic. Until that is done in
// https://www.drupal.org/node/2375689 the access will be set uncacheable.
if ($this->blockAccess($account)) {
$access = AccessResult::allowed();
}
else {
......@@ -179,18 +132,6 @@ public function access(AccountInterface $account) {
return $access->setCacheable(FALSE);
}
/**
* 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.
*
......@@ -287,53 +228,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
$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;
......@@ -362,15 +256,6 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
$contexts = $form_state->getValue(array('cache', 'contexts'));
$form_state->setValue(array('cache', 'contexts'), array_values(array_filter($contexts)));
foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
// Allow the condition to validate the form.
$condition_values = (new FormState())
->setValues($form_state->getValue(['visibility', $condition_id]));
$condition->validateConfigurationForm($form, $condition_values);
// Update the original form values.
$form_state->setValue(['visibility', $condition_id], $condition_values->getValues());
}
$this->blockValidate($form, $form_state);
}
......@@ -394,14 +279,6 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['label_display'] = $form_state->getValue('label_display');
$this->configuration['provider'] = $form_state->getValue('provider');
$this->configuration['cache'] = $form_state->getValue('cache');
foreach ($this->getVisibilityConditions() as $condition_id => $condition) {
// Allow the condition to submit the form.
$condition_values = (new FormState())
->setValues($form_state->getValue(['visibility', $condition_id]));
$condition->submitConfigurationForm($form, $condition_values);
// Update the original form values.
$form_state->setValue(['visibility', $condition_id], $condition_values->getValues());
}
$this->blockSubmit($form, $form_state);
}
}
......@@ -503,61 +380,4 @@ public function isCacheable() {
return $max_age === Cache::PERMANENT || $max_age > 0;
}
/**
* {@inheritdoc}
*/
public function getVisibilityConditions() {
if (!isset($this->conditionCollection)) {
$this->conditionCollection = new ConditionPluginCollection($this->conditionPluginManager(), $this->configuration['visibility']);
}
return $this->conditionCollection;
}
/**
* {@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');
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Block;
use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Cache\CacheableInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
......@@ -143,35 +142,4 @@ public function blockSubmit($form, FormStateInterface $form_state);
*/
public function getMachineNameSuggestion();
/**
* Gets conditions for this block.
*
* @return \Drupal\Core\Condition\ConditionInterface[]|\Drupal\Core\Condition\ConditionPluginCollection
* An array or collection of configured condition plugins.
*/
public function getVisibilityConditions();
/**
* Gets a visibility condition plugin instance.
*
* @param string $instance_id
* The condition plugin instance ID.
*
* @return \Drupal\Core\Condition\ConditionInterface
* A condition plugin.
*/
public function getVisibilityCondition($instance_id);
/**
* Sets the visibility condition configuration.
*
* @param string $instance_id
* The condition instance ID.
* @param array $configuration
* The condition configuration.
*
* @return $this
*/
public function setVisibilityConfig($instance_id, array $configuration);
}
......@@ -9,6 +9,7 @@
use Drupal\Core\Executable\ExecutablePluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
/**
* Provides a basis for fulfilling contexts for condition plugins.
......@@ -21,6 +22,8 @@
*/
abstract class ConditionPluginBase extends ExecutablePluginBase implements ConditionInterface {
use ContextAwarePluginAssignmentTrait;
/**
* {@inheritdoc}
*/
......@@ -41,6 +44,9 @@ public function isNegated() {
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$temporary = $form_state->getTemporary();
$contexts = isset($temporary['gathered_contexts']) ? $temporary['gathered_contexts'] : [];
$form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts);
$form['negate'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Negate the condition'),
......
......@@ -41,6 +41,12 @@ public function getConfiguration() {
$default_config = array();
$default_config['id'] = $instance_id;
$default_config += $this->get($instance_id)->defaultConfiguration();
// In order to determine if a plugin is configured, we must compare it to
// its default configuration. The default configuration of a plugin does
// not contain context_mapping and it is not used when the plugin is not
// configured, so remove the context_mapping from the instance config to
// compare the remaining values.
unset($instance_config['context_mapping']);
if ($default_config === $instance_config) {
unset($configuration[$instance_id]);
}
......
......@@ -291,10 +291,17 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
$values = $form_state->getValues();
if ($this->entity instanceof EntityWithPluginCollectionInterface) {
// Do not manually update values represented by plugin collections.
$values = array_diff_key($values, $this->entity->getPluginCollections());
}
// @todo: This relies on a method that only exists for config and content
// entities, in a different way. Consider moving this logic to a config
// entity specific implementation.
foreach ($form_state->getValues() as $key => $value) {
foreach ($values as $key => $value) {
$entity->set($key, $value);
}
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\ContextAwarePluginInterface;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Utility\String;
......@@ -73,12 +72,7 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
* {@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']);
}
}
$mappings += $plugin->getContextMapping();
$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.
......
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait.
*/
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\ContextAwarePluginInterface;
/**
* Handles context assignments for context-aware plugins.
*/
trait ContextAwarePluginAssignmentTrait {
/**
* Ensures the t() method is available.
*
* @see \Drupal\Core\StringTranslation\StringTranslationTrait
*/
abstract protected function t($string, array $args = array(), array $options = array());
/**
* Wraps the context handler.
*
* @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected function contextHandler() {
return \Drupal::service('context.handler');
}
/**
* Builds a form element for assigning a context to a given slot.
*
* @param \Drupal\Component\Plugin\ContextAwarePluginInterface $plugin
* The context-aware plugin.
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts.
*
* @return array
* A form element for assigning context.
*/
protected function addContextAssignmentElement(ContextAwarePluginInterface $plugin, array $contexts) {
$element = [];
foreach ($plugin->getContextDefinitions() as $context_slot => $definition) {
$valid_contexts = $this->contextHandler()->getMatchingContexts($contexts, $definition);
$options = [];
foreach ($valid_contexts as $context_id => $context) {
$element['#tree'] = TRUE;
$options[$context_id] = $context->getContextDefinition()->getLabel();
$element[$context_slot] = [
'#type' => 'value',
'#value' => $context_id,
];
}
if (count($options) > 1) {
$assignments = $plugin->getContextMapping();
$element[$context_slot] = [
'#title' => $this->t('Select a @context value:', ['@context' => $context_slot]),
'#type' => 'select',
'#options' => $options,
'#required' => $definition->isRequired(),
'#default_value' => !empty($assignments[$context_slot]) ? $assignments[$context_slot] : '',
];
}
}
return $element;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\ContextAwarePluginBase as ComponentContextAwarePluginBase;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
......@@ -49,4 +50,45 @@ public function setContext($name, ComponentContextInterface $context) {
parent::setContext($name, $context);
}
/**
* {@inheritdoc}
*/
public function getContextMapping() {
$configuration = $this instanceof ConfigurablePluginInterface ? $this->getConfiguration() : $this->configuration;
return isset($configuration['context_mapping']) ? array_flip($configuration['context_mapping']) : [];
}
/**
* {@inheritdoc}
*/
public function setContextMapping(array $context_mapping) {
if ($this instanceof ConfigurablePluginInterface) {
$configuration = $this->getConfiguration();
$configuration['context_mapping'] = $context_mapping;
$this->setConfiguration($configuration);
}
else {
$this->configuration['context_mapping'] = $context_mapping;
}
return $this;
}
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface[]
*/
public function getContextDefinitions() {
return parent::getContextDefinitions();
}
/**