Commit 46e6f723 authored by alexpott's avatar alexpott

Issue #2512866 by lauriii, Berdir, Wim Leers, Fabianx, effulgentsia, catch,...

Issue #2512866 by lauriii, Berdir, Wim Leers, Fabianx, effulgentsia, catch, dawehner: CacheContextsManager::optimizeTokens() optimizes ['user', 'user.permissions'] to ['user'] without adding cache tags to invalidate that when the user's roles are modified
parent 0f4319e4
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\PermissionsHashGeneratorInterface;
......@@ -15,12 +16,12 @@
*
* Cache context ID: 'user.permissions'.
*/
class AccountPermissionsCacheContext extends UserCacheContext {
class AccountPermissionsCacheContext extends UserCacheContextBase implements CacheContextInterface {
/**
* The permissions hash generator.
*
* @var \Drupal\user\PermissionsHashInterface
* @var \Drupal\Core\Session\PermissionsHashGeneratorInterface
*/
protected $permissionsHashGenerator;
......@@ -29,7 +30,7 @@ class AccountPermissionsCacheContext extends UserCacheContext {
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
* @param \Drupal\user\PermissionsHashInterface $permissions_hash_generator
* @param \Drupal\Core\Session\PermissionsHashGeneratorInterface $permissions_hash_generator
* The permissions hash generator.
*/
public function __construct(AccountInterface $user, PermissionsHashGeneratorInterface $permissions_hash_generator) {
......@@ -51,4 +52,17 @@ public function getContext() {
return 'ph.' . $this->permissionsHashGenerator->generate($this->user);
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
$cacheable_metadata = new CacheableMetadata();
$tags = [];
foreach ($this->user->getRoles() as $rid) {
$tags[] = "config:user.role.$rid";
}
return $cacheable_metadata->setCacheTags($tags);
}
}
......@@ -31,4 +31,21 @@ public static function getLabel();
*/
public function getContext();
/**
* Gets the cacheability metadata for the context.
*
* There are three valid cases for the returned CacheableMetadata object:
* - An empty object means this can be optimized away safely.
* - A max-age of 0 means that this context can never be optimized away. It
* will never bubble up and cache tags will not be used.
* - Any non-zero max-age and cache tags will bubble up into the cache item
* if this is optimized away to allow for invalidation if the context
* value changes.
*
*
* @return \Drupal\Core\Cache\CacheableMetadata
* A cacheable metadata object.
*/
public function getCacheableMetadata();
}
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheableMetadata;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -99,23 +99,35 @@ public function getLabels($include_calculated_cache_contexts = FALSE) {
* @param string[] $context_tokens
* An array of cache context tokens.
*
* @return string[]
* The array of corresponding cache keys.
* @return \Drupal\Core\Cache\Context\ContextCacheKeys
* The ContextCacheKeys object containing the converted cache keys and
* cacheability metadata.
*
* @throws \InvalidArgumentException
* @throws \LogicException
* Thrown if any of the context tokens or parameters are not valid.
*/
public function convertTokensToKeys(array $context_tokens) {
$context_tokens = $this->optimizeTokens($context_tokens);
sort($context_tokens);
$this->validateTokens($context_tokens);
$cacheable_metadata = new CacheableMetadata();
$optimized_tokens = $this->optimizeTokens($context_tokens);
// Iterate over cache contexts that have been optimized away and get their
// cacheability metadata.
foreach (static::parseTokens(array_diff($context_tokens, $optimized_tokens)) as $context_token) {
list($context_id, $parameter) = $context_token;
$context = $this->getService($context_id);
$cacheable_metadata = $cacheable_metadata->merge($context->getCacheableMetadata($parameter));
}
sort($optimized_tokens);
$keys = [];
foreach (static::parseTokens($context_tokens) as $context) {
foreach (static::parseTokens($optimized_tokens) as $context) {
list($context_id, $parameter) = $context;
if (!in_array($context_id, $this->contexts)) {
throw new \InvalidArgumentException(SafeMarkup::format('"@context" is not a valid cache context ID.', ['@context' => $context_id]));
}
$keys[] = $this->getService($context_id)->getContext($parameter);
}
return $keys;
// Create the returned object and merge in the cacheability metadata.
$context_cache_keys = new ContextCacheKeys($keys);
return $context_cache_keys->merge($cacheable_metadata);
}
/**
......@@ -129,6 +141,9 @@ public function convertTokensToKeys(array $context_tokens) {
* possible of a set of cache context tokens, that still captures the entire
* universe of variations.
*
* If a cache context is being optimized away, it is able to set cacheable
* metadata for itself which will be bubbled up.
*
* E.g. when caching per user ('user'), also caching per role ('user.roles')
* is meaningless because "per role" is implied by "per user".
*
......@@ -150,6 +165,14 @@ public function convertTokensToKeys(array $context_tokens) {
public function optimizeTokens(array $context_tokens) {
$optimized_content_tokens = [];
foreach ($context_tokens as $context_token) {
// Extract the parameter if available.
$parameter = NULL;
$context_id = $context_token;
if (strpos($context_token, ':') !== FALSE) {
list($context_id, $parameter) = explode(':', $context_token);
}
// Context tokens without:
// - a period means they don't have a parent
// - a colon means they're not a specific value of a cache context
......@@ -157,6 +180,11 @@ public function optimizeTokens(array $context_tokens) {
if (strpos($context_token, '.') === FALSE && strpos($context_token, ':') === FALSE) {
$optimized_content_tokens[] = $context_token;
}
// Check cacheability. If the context defines a max-age of 0, then it
// can not be optimized away. Pass the parameter along if we have one.
elseif ($this->getService($context_id)->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
$optimized_content_tokens[] = $context_token;
}
// The context token has a period or a colon. Iterate over all ancestor
// cache contexts. If one exists, omit the context token.
else {
......
......@@ -34,7 +34,32 @@ public static function getLabel();
* @return string
* The string representation of the cache context. When $parameter is NULL,
* a value representing all possible parameters must be generated.
*
* @throws \LogicException
* Thrown if the passed in parameter is invalid.
*/
public function getContext($parameter = NULL);
/**
* Gets the cacheability metadata for the context based on the parameter value.
*
* There are three valid cases for the returned CacheableMetadata object:
* - An empty object means this can be optimized away safely.
* - A max-age of 0 means that this context can never be optimized away. It
* will never bubble up and cache tags will not be used.
* - Any non-zero max-age and cache tags will bubble up into the cache item
* if this is optimized away to allow for invalidation if the context
* value changes.
*
* @param string|null $parameter
* The parameter, or NULL to indicate all possible parameter values.
*
* @return \Drupal\Core\Cache\CacheableMetadata
* A cacheable metadata object.
*
* @throws \LogicException
* Thrown if the passed in parameter is invalid.
*/
public function getCacheableMetadata($parameter = NULL);
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\Context\ContextCacheKeys.
*/
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* A value object to store generated cache keys with its cacheability metadata.
*/
class ContextCacheKeys extends CacheableMetadata {
/**
* The generated cache keys.
*
* @var string[]
*/
protected $keys;
/**
* Constructs a ContextCacheKeys object.
*
* @param string[] $keys
* The cache context keys.
*/
public function __construct(array $keys) {
$this->keys = $keys;
}
/**
* Gets the generated cache keys.
*
* @return string[]
* The cache keys.
*/
public function getKeys() {
return $this->keys;
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the CookiesCacheContext service, for "per cookie" caching.
*
......@@ -35,4 +37,11 @@ public function getContext($cookie = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($cookie = NULL) {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the HeadersCacheContext service, for "per header" caching.
*
......@@ -35,4 +37,11 @@ public function getContext($header = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($header = NULL) {
return new CacheableMetadata();
}
}
......@@ -7,12 +7,14 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the IpCacheContext service, for "per IP address" caching.
*
* Cache context ID: 'ip'.
*/
class IpCacheContext extends RequestStackCacheContextBase {
class IpCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
......@@ -28,4 +30,11 @@ public function getContext() {
return $this->requestStack->getCurrentRequest()->getClientIp();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,12 +7,14 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the IsSuperUserCacheContext service, for "super user or not" caching.
*
* Cache context ID: 'user.is_super_user'.
*/
class IsSuperUserCacheContext extends UserCacheContext {
class IsSuperUserCacheContext extends UserCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
......@@ -28,4 +30,11 @@ public function getContext() {
return ((int) $this->user->id()) === 1 ? '1' : '0';
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Language\LanguageManagerInterface;
/**
......@@ -74,4 +75,11 @@ public function getContext($type = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($type = NULL) {
return new CacheableMetadata();
}
}
......@@ -7,12 +7,13 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
use Symfony\Component\DependencyInjection\ContainerAware;
/**
* Defines the MenuActiveTrailsCacheContext service.
*
* This class is container-aware to avoid initializing the 'menu.active_trail'
* This class is container-aware to avoid initializing the 'menu.active_trails'
* service (and its dependencies) when it is not necessary.
*/
class MenuActiveTrailsCacheContext extends ContainerAware implements CalculatedCacheContextInterface {
......@@ -28,9 +29,24 @@ public static function getLabel() {
* {@inheritdoc}
*/
public function getContext($menu_name = NULL) {
if (!$menu_name) {
throw new \LogicException('No menu name provided for menu.active_trails cache context.');
}
$active_trail = $this->container->get('menu.active_trail')
->getActiveTrailIds($menu_name);
return 'menu_trail.' . $menu_name . '|' . implode('|', $active_trail);
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($menu_name = NULL) {
if (!$menu_name) {
throw new \LogicException('No menu name provided for menu.active_trails cache context.');
}
$cacheable_metadata = new CacheableMetadata();
return $cacheable_metadata->setCacheTags(["config:system.menu.$menu_name"]);
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines a cache context for "per page in a pager" caching.
*
......@@ -38,4 +40,11 @@ public function getContext($pager_id = NULL) {
return 'pager.' . $pager_id . '.' . pager_find_page($pager_id);
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($pager_id = NULL) {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the QueryArgsCacheContext service, for "per query args" caching.
*
......@@ -35,4 +37,11 @@ public function getContext($query_arg = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($query_arg = NULL) {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the RequestFormatCacheContext service, for "per format" caching.
*
......@@ -28,4 +30,11 @@ public function getContext() {
return $this->requestStack->getCurrentRequest()->getRequestFormat();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -11,8 +11,12 @@
/**
* Defines a base class for cache contexts depending only on the request stack.
*
* Subclasses need to implement either
* \Drupal\Core\Cache\Context\CacheContextInterface or
* \Drupal\Core\Cache\Context\CalculatedCacheContextInterface.
*/
abstract class RequestStackCacheContextBase implements CacheContextInterface {
abstract class RequestStackCacheContextBase {
/**
* The request stack.
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Routing\RouteMatchInterface;
/**
......@@ -47,4 +48,11 @@ public function getContext() {
return $this->routeMatch->getRouteName() . hash('sha256', serialize($this->routeMatch->getRawParameters()->all()));
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the SiteCacheContext service, for "per site" caching.
*
......@@ -20,7 +22,7 @@
* @see \Symfony\Component\HttpFoundation\Request::getSchemeAndHttpHost()
* @see \Symfony\Component\HttpFoundation\Request::getBaseUrl()
*/
class SiteCacheContext extends RequestStackCacheContextBase {
class SiteCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
......@@ -37,4 +39,11 @@ public function getContext() {
return $request->getSchemeAndHttpHost() . $request->getBaseUrl();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
......@@ -48,4 +48,11 @@ public function getContext() {
return $this->themeManager->getActiveTheme()->getName() ?: 'stark';
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the TimeZoneCacheContext service, for "per time zone" caching.
*
......@@ -32,4 +34,11 @@ public function getContext() {
return date_default_timezone_get();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,12 +7,14 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the UrlCacheContext service, for "per page" caching.
*
* Cache context ID: 'url'.
*/
class UrlCacheContext extends RequestStackCacheContextBase {
class UrlCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
......@@ -28,4 +30,11 @@ public function getContext() {
return $this->requestStack->getCurrentRequest()->getUri();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
......@@ -7,24 +7,14 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the UserCacheContext service, for "per user" caching.
*
* Cache context ID: 'user'.
*/
class UserCacheContext implements CacheContextInterface {
/**
* Constructs a new UserCacheContext service.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(AccountInterface $user) {
$this->user = $user;
}
class UserCacheContext extends UserCacheContextBase implements CacheContextInterface {
/**
* {@inheritdoc}
......@@ -40,4 +30,11 @@ public function getContext() {
return "u." . $this->user->id();
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\Context\UserCacheContextBase.
*/
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Session\AccountInterface;
/**
* Base class for user-based cache contexts.
*
* Subclasses need to implement either
* \Drupal\Core\Cache\Context\CacheContextInterface or
* \Drupal\Core\Cache\Context\CalculatedCacheContextInterface.
*/
abstract class UserCacheContextBase {
/**
* The account object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* Constructs a new UserCacheContextBase class.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(AccountInterface $user) {
$this->user = $user;
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the UserRolesCacheContext service, for "per role" caching.
*
......@@ -17,7 +19,7 @@
* Calculated cache context ID: 'user.roles:%role', e.g. 'user.roles:anonymous'
* (to vary by the presence/absence of a specific role).
*/
class UserRolesCacheContext extends UserCacheContext implements CalculatedCacheContextInterface{
class UserRolesCacheContext extends UserCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
......@@ -45,4 +47,11 @@ public function getContext($role = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($role = NULL) {
return new CacheableMetadata();
}
}
......@@ -86,7 +86,7 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex
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
// They also need to know about the cacheability 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) {
......
......@@ -289,13 +289,13 @@ protected function maxAgeToExpire($max_age) {
*
* Creates the cache ID string based on #cache['keys'] + #cache['contexts'].
*
* @param array $elements