diff --git a/core/core.services.yml b/core/core.services.yml index d17f901..ceabcfb 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -5,6 +5,15 @@ parameters: default: keyvalue.database factory.keyvalue.expirable: default: keyvalue.expirable.database + # All the 'language.*' parameters must be overridden by the module providing + # the default language manager. They are necessary for allowing services to be + # language-aware in an efficient manner. + # @see \Drupal\language\LanguageServiceProvider::alter() + # @see \Drupal\Core\DrupalKernel::compileContainer() + language.default_values: [] + language.is_multilingual: false + language.supported_langcodes: [] + language.langcode_mappings: {} services: cache_factory: class: Drupal\Core\Cache\CacheFactory diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index bc36c15..da25e14 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -980,6 +980,24 @@ protected function compileContainer() { } $container->setParameter('container.namespaces', $namespaces); + // Register synthetic services. + $container->register('class_loader')->setSynthetic(TRUE); + $container->register('kernel', 'Symfony\Component\HttpKernel\KernelInterface')->setSynthetic(TRUE); + $container->register('service_container', 'Symfony\Component\DependencyInjection\ContainerInterface')->setSynthetic(TRUE); + + // Register declarative application services first, then set the default + // language values container parameter, and then register the application + // services defined in service providers. + // This particular order allows: + // - the declarative application services to document all container; + // parameters + // - the application service providers to override all container parameters; + // - while still performing the low-level initialization of the default + // language values container parameter. + $yaml_loader = new YamlFileLoader($container); + foreach ($this->serviceYamls['app'] as $filename) { + $yaml_loader->load($filename); + } // Store the default language values on the container. This is so that the // default language can be configured using the configuration factory. This // avoids the circular dependencies that would created by @@ -992,17 +1010,6 @@ protected function compileContainer() { } } $container->setParameter('language.default_values', $default_language_values); - - // Register synthetic services. - $container->register('class_loader')->setSynthetic(TRUE); - $container->register('kernel', 'Symfony\Component\HttpKernel\KernelInterface')->setSynthetic(TRUE); - $container->register('service_container', 'Symfony\Component\DependencyInjection\ContainerInterface')->setSynthetic(TRUE); - - // Register application services. - $yaml_loader = new YamlFileLoader($container); - foreach ($this->serviceYamls['app'] as $filename) { - $yaml_loader->load($filename); - } foreach ($this->serviceProviders['app'] as $provider) { if ($provider instanceof ServiceProviderInterface) { $provider->register($container); diff --git a/core/modules/language/src/LanguageNegotiator.php b/core/modules/language/src/LanguageNegotiator.php index e5ff4e5..2354d88 100644 --- a/core/modules/language/src/LanguageNegotiator.php +++ b/core/modules/language/src/LanguageNegotiator.php @@ -188,27 +188,7 @@ protected function negotiateLanguage($type, $method_id) { $method = $this->negotiatorManager->getDefinition($method_id); if (!isset($method['types']) || in_array($type, $method['types'])) { - - // @todo This entire chunk is problematic… - // Check for a cache mode force from settings.php. - if ($this->settings->get('page_cache_without_database')) { - $cache_enabled = TRUE; - } - else { - $cache_enabled = $this->configFactory->get('system.performance')->get('cache.page.use_internal'); - } - - // If the language negotiation method has no cache preference or this is - // satisfied we can execute the callback. - // @todo … because 1) it hardcodes the internal page cache so it assumes - // page caching doesn't live in a module, 2) what about other page - // cache implementations, like boost? - // Note also that it makes an exception for LanguageNegotiationBrowser, - // which apparently cannot be cached, and therefore sets "cache = 0" in - // its annotation, which is detected here. - if ($cache = !isset($method['cache']) || $this->currentUser->isAuthenticated() || $method['cache'] == $cache_enabled) { - $langcode = $this->getNegotiationMethodInstance($method_id)->getLangcode($this->requestStack->getCurrentRequest()); - } + $langcode = $this->getNegotiationMethodInstance($method_id)->getLangcode($this->requestStack->getCurrentRequest()); } $languages = $this->languageManager->getLanguages(); diff --git a/core/modules/language/src/LanguageServiceProvider.php b/core/modules/language/src/LanguageServiceProvider.php index d7c4ddd..11c89a1 100644 --- a/core/modules/language/src/LanguageServiceProvider.php +++ b/core/modules/language/src/LanguageServiceProvider.php @@ -53,9 +53,18 @@ public function alter(ContainerBuilder $container) { ->addArgument(new Reference('module_handler')) ->addArgument(new Reference('language.config_factory_override')) ->addArgument(new Reference('request_stack')); + + // Allow other services to be aware of: + // - the default language; + // - whether the site is multilingual; + // - the list of supported langcodes; + // - the hash of browser langcodes to standardized/Drupal langcodes. if ($default_language_values = $this->getDefaultLanguageValues()) { $container->setParameter('language.default_values', $default_language_values); } + $container->setParameter('language.is_multilingual', $this->isMultilingual()); + $container->setParameter('language.supported_langcodes', $this->getLanguageCodes()); + $container->setParameter('language.langcode_mappings', $this->getLangcodeMappings()); // For monolingual sites, we explicitly set the default language for the // language config override service as there is no language negotiation. @@ -67,12 +76,20 @@ public function alter(ContainerBuilder $container) { } /** - * Checks whether the site is multilingual. + * Returns a list of langcodes, of the languages set up on the site. * - * @return bool - * TRUE if the site is multilingual, FALSE otherwise. + * Returns the equivalent of + * @code + * array_keys(LanguageManagerInterface::getLanguages()) + * @endcode + * but without needing the container to be available. + * + * @return string[] + * A list of language codes. + * + * @see \Drupal\Core\Language\LanguageManagerInterface::getLanguages */ - protected function isMultilingual() { + protected function getLanguageCodes() { // Assign the prefix to a local variable so it can be used in an anonymous // function. $prefix = static::CONFIG_PREFIX; @@ -83,7 +100,35 @@ protected function isMultilingual() { $config_ids = array_filter($config_storage->listAll($prefix), function($config_id) use ($prefix) { return $config_id != $prefix . LanguageInterface::LANGCODE_NOT_SPECIFIED && $config_id != $prefix . LanguageInterface::LANGCODE_NOT_APPLICABLE; }); - return count($config_ids) > 1; + $languages = array_map(function ($config_id) use ($prefix) { + return str_replace($prefix, '', $config_id); + }, $config_ids); + return $languages; + } + + /** + * Checks whether the site is multilingual. + * + * @return bool + * TRUE if the site is multilingual, FALSE otherwise. + */ + protected function isMultilingual() { + return count($this->getLanguageCodes()) > 1; + } + + /** + * Returns the list of langcode mappings, if any. + * + * @return string[] + * An array with browser langcodes as keys and standardized/Drupal langcodes + * as values. + */ + protected function getLangcodeMappings() { + // @todo Try to swap out for config.storage to take advantage of database + // and caching. This might prove difficult as this is called before the + // container has finished building. + $config_storage = BootstrapConfigStorageFactory::get(); + return $config_storage->read('language.mappings') ?: []; } /** diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php index e9fc38e..462646a 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php @@ -17,7 +17,6 @@ * @Plugin( * id = \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationBrowser::METHOD_ID, * weight = -2, - * cache = 0, * name = @Translation("Browser"), * description = @Translation("Language from the browser's language settings."), * config_route_name = "language.negotiation_browser" diff --git a/core/modules/page_cache/page_cache.services.yml b/core/modules/page_cache/page_cache.services.yml index 3ac1d24..d532167 100644 --- a/core/modules/page_cache/page_cache.services.yml +++ b/core/modules/page_cache/page_cache.services.yml @@ -1,6 +1,6 @@ services: http_middleware.page_cache: class: Drupal\page_cache\StackMiddleware\PageCache - arguments: ['@cache.render', '@page_cache_request_policy', '@page_cache_response_policy', '@content_negotiation'] + arguments: ['@cache.render', '@page_cache_request_policy', '@page_cache_response_policy', '@content_negotiation', '%language.is_multilingual%', '%language.supported_langcodes%', '%language.langcode_mappings%', '%language.default_values%'] tags: - { name: http_middleware, priority: 200 } diff --git a/core/modules/page_cache/src/StackMiddleware/PageCache.php b/core/modules/page_cache/src/StackMiddleware/PageCache.php index 66a5f88..a821a39 100644 --- a/core/modules/page_cache/src/StackMiddleware/PageCache.php +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php @@ -7,6 +7,7 @@ namespace Drupal\page_cache\StackMiddleware; +use Drupal\Component\Utility\UserAgent; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\ContentNegotiation; @@ -60,6 +61,34 @@ class PageCache implements HttpKernelInterface { protected $contentNegotiation; /** + * The language.is_multilingual container parameter. + * + * @var bool + */ + protected $isMultilingual; + + /** + * The language.supported_langcodes container parameter. + * + * @var string[] + */ + protected $supportedLangcodes; + + /** + * The language.langcode_mappings container parameter. + * + * @var \string[] + */ + protected $langcodeMappings; + + /** + * The language.default-values container parameter. + * + * @var array + */ + protected $defaultLanguageValues; + + /** * Constructs a PageCache object. * * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel @@ -72,13 +101,25 @@ class PageCache implements HttpKernelInterface { * A policy rule determining the cacheability of the response. * @param \Drupal\Core\ContentNegotiation $content_negotiation * The content negotiation library. + * @param bool $is_multilingual + * The language.is_multilingual container parameter. + * @param string[] $supported_langcodes + * The language.supported_langcodes container parameter. + * @param string[] $langcode_mappings + * The language.langcode_mappings container parameter. + * @param array $language_default_values + * The language.default-values container parameter. */ - public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, ContentNegotiation $content_negotiation) { + public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, ContentNegotiation $content_negotiation, $is_multilingual, array $supported_langcodes, array $langcode_mappings, array $language_default_values) { $this->httpKernel = $http_kernel; $this->cache = $cache; $this->requestPolicy = $request_policy; $this->responsePolicy = $response_policy; $this->contentNegotiation = $content_negotiation; + $this->isMultilingual = $is_multilingual; + $this->supportedLangcodes = $supported_langcodes; + $this->langcodeMappings = $langcode_mappings; + $this->defaultLanguageValues = $language_default_values; } /** @@ -308,19 +349,24 @@ protected function getCacheId(Request $request) { $request->getUri(), $this->contentNegotiation->getContentType($request), ); - return implode(':', $cid_parts); - } - /** - * Wraps Drupal::config(). - * - * Config factory is not injected into this class in order to prevent - * premature initialization of config storage (database). - * - * @see \Drupal::config() - */ - protected function config($name) { - return \Drupal::config($name); + // If there is an Accept-Language header, make it part of the CID. + $http_accept_language = $request->server->get('HTTP_ACCEPT_LANGUAGE'); + if ($http_accept_language) { + $default_langcode = $this->defaultLanguageValues['id']; + if ($this->isMultilingual) { + $langcode = UserAgent::getBestMatchingLangcode($http_accept_language, $this->supportedLangcodes, $this->langcodeMappings); + if ($langcode === FALSE) { + $langcode = $default_langcode; + } + } + else { + $langcode = $default_langcode; + } + $cid_parts[] = $langcode; + } + + return implode(':', $cid_parts); } } diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 8e59765..229eef5 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -8,7 +8,10 @@ namespace Drupal\page_cache\Tests; use Drupal\Component\Datetime\DateTimePlus; +use Drupal\Component\Utility\String; use Drupal\Core\Routing\RequestContext; +use Drupal\Core\Url; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\Core\Cache\Cache; @@ -95,6 +98,64 @@ function testAcceptHeaderRequests() { } /** + * Tests support for different cache items with different Accept-Language headers. + * + * Note: test coverage for all the possible Accept-Language cases lives at + * \Drupal\Tests\Component\Utility\UserAgentTest. + */ + function testAcceptLanguageHeaderRequests() { + $url = Url::fromRoute(''); + + $verify_page_cache = function($expected_cid_parts, $accepted_langcode = FALSE) use ($url) { + $headers = []; + if (is_string($accepted_langcode)) { + $headers[] = 'Accept-Language: ' . $accepted_langcode; + } + $this->drupalGet($url, [], $headers); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'HTML page was not yet cached.'); + $this->drupalGet($url, [], $headers); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'HTML page was cached.'); + $cid = implode(':', $expected_cid_parts); + $this->assertTrue(\Drupal::cache('render')->get($cid), String::format('Expected page cache item exists with CID %cid.', ['%cid' => $cid])); + + \Drupal::cache('render')->deleteAll(); + }; + + // Single-language site: + // - no Accept-Language header: not keyed by langcode. + $verify_page_cache([$url->setAbsolute()->toString(), 'html']); + // - nonsensical Accept-Language header: keyed by default ('en'). + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'utterly wrong Accept-Language header value, but presence matters in this test'); + // - any valid Accept-Language language: keyed by default ('en'). + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'en'); + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'nl'); + + // Multilingual site, single lang: + // - nonsensical Accept-Language header: keyed by default ('en'). + $this->container->get('module_installer')->install(['language']); + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'utterly wrong Accept-Language header value, but presence matters in this test'); + // - 'en' Accept-Language header: keyed by default ('en'). + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'en'); + // - 'nl' Accept-Language header: keyed by default ('en'). + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'nl'); + + // Multilingual site, multiple langs: + // - nonsensical Accept-Language header: keyed by default ('en'). + ConfigurableLanguage::createFromLangcode('nl')->save(); + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'utterly wrong Accept-Language header value, but presence matters in this test'); + // - 'en' Accept-Language header: is also best match. + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'en'); + // - 'nl' Accept-Language header: is also best match. + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'nl'], 'nl'); + // - 'en-US' Accept-Language header: matched to 'en'. + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'en'], 'en-US'); + // - 'nl-BE' Accept-Language header: matched to 'nl'. + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'nl'], 'nl-BE'); + // - complex Accept-Language header: matched to 'nl'. + $verify_page_cache([$url->setAbsolute()->toString(), 'html', 'nl'], 'hu, nl-BE;q=0.66, en;q=0.33'); + } + + /** * Tests support of requests with If-Modified-Since and If-None-Match headers. */ function testConditionalRequests() {