Commit 7543540e authored by alexpott's avatar alexpott
Browse files

Issue #2375695 by Berdir, EclipseGc, tim.plunkett, Wim Leers, Fabianx,...

Issue #2375695 by Berdir, EclipseGc, tim.plunkett, Wim Leers, Fabianx, dawehner: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed
parent 05aa5ea1
...@@ -22,7 +22,7 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware ...@@ -22,7 +22,7 @@ abstract class ContextAwarePluginBase extends PluginBase implements ContextAware
* *
* @var \Drupal\Component\Plugin\Context\ContextInterface[] * @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/ */
protected $context; protected $context = [];
/** /**
* Overrides \Drupal\Component\Plugin\PluginBase::__construct(). * Overrides \Drupal\Component\Plugin\PluginBase::__construct().
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use Drupal\block\BlockInterface; use Drupal\block\BlockInterface;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginBase; use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
...@@ -272,6 +273,16 @@ public function getMachineNameSuggestion() { ...@@ -272,6 +273,16 @@ public function getMachineNameSuggestion() {
return $transliterated; return $transliterated;
} }
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$max_age = parent::getCacheMaxAge();
// @todo Configurability of this will be removed in
// https://www.drupal.org/node/2458763.
return Cache::mergeMaxAges($max_age, (int) $this->configuration['cache']['max_age']);
}
/** /**
* Wraps the transliteration service. * Wraps the transliteration service.
* *
...@@ -294,25 +305,4 @@ public function setTransliteration(TransliterationInterface $transliteration) { ...@@ -294,25 +305,4 @@ public function setTransliteration(TransliterationInterface $transliteration) {
$this->transliteration = $transliteration; $this->transliteration = $transliteration;
} }
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return (int)$this->configuration['cache']['max_age'];
}
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Executable\ExecutableInterface; use Drupal\Core\Executable\ExecutableInterface;
use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Plugin\PluginFormInterface;
...@@ -46,7 +47,7 @@ ...@@ -46,7 +47,7 @@
* *
* @ingroup plugin_api * @ingroup plugin_api
*/ */
interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface { interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface, CacheableDependencyInterface {
/** /**
* Determines whether condition result will be negated. * Determines whether condition result will be negated.
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
namespace Drupal\Core\Condition; namespace Drupal\Core\Condition;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Executable\ExecutablePluginBase; use Drupal\Core\Executable\ExecutablePluginBase;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
use Drupal\Component\Plugin\Context\Context as ComponentContext; use Drupal\Component\Plugin\Context\Context as ComponentContext;
use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataTrait; use Drupal\Core\TypedData\TypedDataTrait;
...@@ -34,6 +36,21 @@ class Context extends ComponentContext implements ContextInterface { ...@@ -34,6 +36,21 @@ class Context extends ComponentContext implements ContextInterface {
*/ */
protected $contextDefinition; protected $contextDefinition;
/**
* The cacheability metadata.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $cacheabilityMetadata;
/**
* {@inheritdoc}
*/
public function __construct(ContextDefinitionInterface $context_definition) {
parent::__construct($context_definition);
$this->cacheabilityMetadata = new CacheableMetadata();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -67,6 +84,11 @@ public function hasContextValue() { ...@@ -67,6 +84,11 @@ public function hasContextValue() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function setContextValue($value) { public function setContextValue($value) {
// Add the value as a cacheable dependency only if implements the interface
// to prevent it from disabling caching with a max-age 0.
if ($value instanceof CacheableDependencyInterface) {
$this->addCacheableDependency($value);
}
if ($value instanceof TypedDataInterface) { if ($value instanceof TypedDataInterface) {
return $this->setContextData($value); return $this->setContextData($value);
} }
...@@ -120,4 +142,33 @@ public function validate() { ...@@ -120,4 +142,33 @@ public function validate() {
return $this->getContextData()->validate(); return $this->getContextData()->validate();
} }
/**
* {@inheritdoc}
*/
public function addCacheableDependency($dependency) {
$this->cacheabilityMetadata = $this->cacheabilityMetadata->merge(CacheableMetadata::createFromObject($dependency));
return $this;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->cacheabilityMetadata->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->cacheabilityMetadata->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->cacheabilityMetadata->getCacheMaxAge();
}
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface;
/** /**
...@@ -84,6 +85,14 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex ...@@ -84,6 +85,14 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex
// This assignment has been used, remove it. // This assignment has been used, remove it.
unset($mappings[$plugin_context_id]); unset($mappings[$plugin_context_id]);
// Plugins have their on context objects, only the value is applied.
// They also need to know about the cacheable metadata of where that
// value is coming from, so pass them through to those objects.
$plugin_context = $plugin->getContext($plugin_context_id);
if ($plugin_context instanceof ContextInterface && $contexts[$context_id] instanceof CacheableDependencyInterface) {
$plugin_context->addCacheableDependency($contexts[$context_id]);
}
// Pass the value to the plugin if there is one. // Pass the value to the plugin if there is one.
if ($contexts[$context_id]->hasContextValue()) { if ($contexts[$context_id]->hasContextValue()) {
$plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue()); $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
......
...@@ -8,12 +8,13 @@ ...@@ -8,12 +8,13 @@
namespace Drupal\Core\Plugin\Context; namespace Drupal\Core\Plugin\Context;
use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface; use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface;
/** /**
* Interface for context. * Interface for context.
*/ */
interface ContextInterface extends ComponentContextInterface { interface ContextInterface extends ComponentContextInterface, CacheableDependencyInterface {
/** /**
* Gets the context value as typed data object. * Gets the context value as typed data object.
...@@ -32,4 +33,22 @@ public function getContextData(); ...@@ -32,4 +33,22 @@ public function getContextData();
*/ */
public function setContextData(TypedDataInterface $data); public function setContextData(TypedDataInterface $data);
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* E.g. when a context depends on some configuration, an entity, or an access
* result, we must make sure their cacheability metadata is present on the
* response. This method makes doing that simple.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
* The dependency. If the object implements CacheableDependencyInterface,
* then its cacheability metadata will be used. Otherwise, the passed in
* object must be assumed to be uncacheable, so max-age 0 is set.
*
* @return $this
*
* @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
*/
public function addCacheableDependency($dependency);
} }
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\ContextAwarePluginBase as ComponentContextAwarePluginBase; use Drupal\Component\Plugin\ContextAwarePluginBase as ComponentContextAwarePluginBase;
use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
...@@ -91,4 +93,53 @@ protected function contextHandler() { ...@@ -91,4 +93,53 @@ protected function contextHandler() {
return \Drupal::service('context.handler'); return \Drupal::service('context.handler');
} }
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$cache_contexts = [];
// Applied contexts can affect the cache contexts when this plugin is
// involved in caching, collect and return them.
foreach ($this->getContexts() as $context) {
/** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
if ($context instanceof CacheableDependencyInterface) {
$cache_contexts = Cache::mergeContexts($cache_contexts, $context->getCacheContexts());
}
}
return $cache_contexts;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = [];
// Applied contexts can affect the cache tags when this plugin is
// involved in caching, collect and return them.
foreach ($this->getContexts() as $context) {
/** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
if ($context instanceof CacheableDependencyInterface) {
$tags = Cache::mergeTags($tags, $context->getCacheTags());
}
}
return $tags;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$max_age = Cache::PERMANENT;
// Applied contexts can affect the cache max age when this plugin is
// involved in caching, collect and return them.
foreach ($this->getContexts() as $context) {
/** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
if ($context instanceof CacheableDependencyInterface) {
$max_age = Cache::mergeMaxAges($max_age, $context->getCacheMaxAge());
}
}
return $max_age;
}
} }
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Condition\ConditionAccessResolverTrait; use Drupal\Core\Condition\ConditionAccessResolverTrait;
use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityHandlerInterface;
...@@ -87,31 +89,60 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A ...@@ -87,31 +89,60 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
else { else {
$contexts = $entity->getContexts(); $contexts = $entity->getContexts();
$conditions = []; $conditions = [];
$missing_context = FALSE;
foreach ($entity->getVisibilityConditions() as $condition_id => $condition) { foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
if ($condition instanceof ContextAwarePluginInterface) { if ($condition instanceof ContextAwarePluginInterface) {
try { try {
$this->contextHandler->applyContextMapping($condition, $contexts); $this->contextHandler->applyContextMapping($condition, $contexts);
} }
catch (ContextException $e) { catch (ContextException $e) {
return AccessResult::forbidden()->setCacheMaxAge(0); $missing_context = TRUE;
} }
} }
$conditions[$condition_id] = $condition; $conditions[$condition_id] = $condition;
} }
if ($this->resolveConditions($conditions, 'and') !== FALSE) {
if ($missing_context) {
// If any context is missing then we might be missing cacheable
// metadata, and don't know based on what conditions the block is
// accessible or not. For example, blocks that have a node type
// condition will have a missing context on any non-node route like the
// frontpage.
// @todo Avoid setting max-age 0 for some or all cases, for example by
// treating available contexts without value differently in
// https://www.drupal.org/node/2521956.
$access = AccessResult::forbidden()->setCacheMaxAge(0);
}
elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
// Delegate to the plugin. // Delegate to the plugin.
$access = $entity->getPlugin()->access($account, TRUE); $access = $entity->getPlugin()->access($account, TRUE);
} }
else { else {
$access = AccessResult::forbidden(); $access = AccessResult::forbidden();
} }
// 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, $this->mergeCacheabilityFromConditions($access, $conditions);
// otherwise it will be impossible to determine by which cache contexts the
// result should be varied. // Ensure that access is evaluated again when the block changes.
// @todo Change this to use $access->cacheUntilEntityChanges($entity) once return $access->cacheUntilEntityChanges($entity);
// https://www.drupal.org/node/2375695 is resolved. }
return $access->setCacheMaxAge(0); }
/**
* Merges cacheable metadata from conditions onto the access result object.
*
* @param \Drupal\Core\Access\AccessResult $access
* The access result object.
* @param \Drupal\Core\Condition\ConditionInterface[] $conditions
* List of visibility conditions.
*/
protected function mergeCacheabilityFromConditions(AccessResult $access, array $conditions) {
foreach ($conditions as $condition) {
if ($condition instanceof CacheableDependencyInterface) {
$access->addCacheTags($condition->getCacheTags());
$access->addCacheContexts($condition->getCacheContexts());
$access->setCacheMaxAge(Cache::mergeMaxAges($access->getCacheMaxAge(), $condition->getCacheMaxAge()));
}
} }
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\block; namespace Drupal\block;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Core\Theme\ThemeManagerInterface;
...@@ -49,7 +50,7 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager ...@@ -49,7 +50,7 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getVisibleBlocksPerRegion(array $contexts) { public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []) {
$active_theme = $this->themeManager->getActiveTheme(); $active_theme = $this->themeManager->getActiveTheme();
// Build an array of the region names in the right order. // Build an array of the region names in the right order.
$empty = array_fill_keys($active_theme->getRegions(), array()); $empty = array_fill_keys($active_theme->getRegions(), array());
...@@ -57,9 +58,19 @@ public function getVisibleBlocksPerRegion(array $contexts) { ...@@ -57,9 +58,19 @@ public function getVisibleBlocksPerRegion(array $contexts) {
$full = array(); $full = array();
foreach ($this->blockStorage->loadByProperties(array('theme' => $active_theme->getName())) as $block_id => $block) { foreach ($this->blockStorage->loadByProperties(array('theme' => $active_theme->getName())) as $block_id => $block) {
/** @var \Drupal\block\BlockInterface $block */ /** @var \Drupal\block\BlockInterface $block */
$block->setContexts($contexts);
$access = $block->access('view', NULL, TRUE);
$region = $block->getRegion();
if (!isset($cacheable_metadata[$region])) {
$cacheable_metadata[$region] = CacheableMetadata::createFromObject($access);
}
else {
$cacheable_metadata[$region] = $cacheable_metadata[$region]->merge(CacheableMetadata::createFromObject($access));
}
// Set the contexts on the block before checking access. // Set the contexts on the block before checking access.
if ($block->setContexts($contexts)->access('view')) { if ($access->isAllowed()) {
$full[$block->getRegion()][$block_id] = $block; $full[$region][$block_id] = $block;
} }
} }
......
...@@ -14,11 +14,14 @@ interface BlockRepositoryInterface { ...@@ -14,11 +14,14 @@ interface BlockRepositoryInterface {
* *
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts to set on the blocks. * An array of contexts to set on the blocks.
* @param \Drupal\Core\Cache\CacheableMetadata[] $cacheable_metadata
* (optional) List of CacheableMetadata objects, keyed by region. This is
* by reference and is used to pass this information back to the caller.
* *
* @return array * @return array
* The array is first keyed by region machine name, with the values * The array is first keyed by region machine name, with the values
* containing an array keyed by block ID, with block entities as the values. * containing an array keyed by block ID, with block entities as the values.
*/ */
public function getVisibleBlocksPerRegion(array $contexts); public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []);
} }
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\block\EventSubscriber; namespace Drupal\block\EventSubscriber;
use Drupal\block\Event\BlockContextEvent; use Drupal\block\Event\BlockContextEvent;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\ContextDefinition;
...@@ -48,6 +49,11 @@ public function onBlockActiveContext(BlockContextEvent $event) { ...@@ -48,6 +49,11 @@ public function onBlockActiveContext(BlockContextEvent $event) {
if (isset($info[$type_key]['name'])) { if (isset($info[$type_key]['name'])) {
$context = new Context(new ContextDefinition('language', $info[$type_key]['name'])); $context = new Context(new ContextDefinition('language', $info[$type_key]['name']));
$context->setContextValue($this->languageManager->getCurrentLanguage($type_key)); $context->setContextValue($this->languageManager->getCurrentLanguage($type_key));
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['languages:' . $type_key]);
$context->addCacheableDependency($cacheability);
$event->setContext('language.' . $type_key, $context); $event->setContext('language.' . $type_key, $context);
} }
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\block\EventSubscriber; namespace Drupal\block\EventSubscriber;
use Drupal\block\Event\BlockContextEvent; use Drupal\block\Event\BlockContextEvent;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\ContextDefinition;
...@@ -56,6 +57,9 @@ public function onBlockActiveContext(BlockContextEvent $event) { ...@@ -56,6 +57,9 @@ public function onBlockActiveContext(BlockContextEvent $event) {
$context = new Context(new ContextDefinition('entity:user', $this->t('Current user'))); $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')));
$context->setContextValue($current_user); $context->setContextValue($current_user);
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
$context->addCacheableDependency($cacheability);
$event->setContext('user.current_user', $context); $event->setContext('user.current_user', $context);
}