summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2015-11-12 22:14:00 (GMT)
committerAlex Pott2015-11-12 22:14:14 (GMT)
commit0e507e421b669d95e7bfe34e780b047bffcb9ac9 (patch)
tree6b1de0e89b00ce8eec7f4c1c871b1990e95a13e9
parent20621c288bc8859edadd0aa05399e18e776c21a5 (diff)
Issue #1810394 by hchonov, plach, YesCT, David Hernández, Schnitzel, vijaycs85, penyaskito, tstoeckler, alexpott, Gábor Hojtsy: Site configuration with domain based language negotiation results in redirecting authenticated users to a different domain when accessing a content entity route for translation language different from the interface language
-rw-r--r--core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php23
-rw-r--r--core/lib/Drupal/Core/Routing/UrlGenerator.php3
-rw-r--r--core/modules/content_translation/content_translation.install16
-rw-r--r--core/modules/content_translation/content_translation.module8
-rw-r--r--core/modules/content_translation/src/ContentTranslationHandler.php4
-rw-r--r--core/modules/content_translation/src/Controller/ContentTranslationController.php6
-rw-r--r--core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php6
-rw-r--r--core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php19
-rw-r--r--core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php8
-rw-r--r--core/modules/language/language.install13
-rw-r--r--core/modules/language/language.services.yml2
-rw-r--r--core/modules/language/src/EventSubscriber/ConfigSubscriber.php40
-rw-r--r--core/modules/language/src/HttpKernel/PathProcessorLanguage.php32
-rw-r--r--core/modules/language/src/LanguageServiceProvider.php4
-rw-r--r--core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php294
-rw-r--r--core/modules/language/src/Tests/EntityUrlLanguageTest.php118
-rw-r--r--core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php182
-rw-r--r--core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php7
-rw-r--r--core/modules/node/src/Tests/NodeTranslationUITest.php3
-rw-r--r--core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php1
-rw-r--r--core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php7
21 files changed, 742 insertions, 54 deletions
diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
index 59f2d0c..cad93c2 100644
--- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
+++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
@@ -21,8 +21,27 @@ interface OutboundPathProcessorInterface {
* @param string $path
* The path to process, with a leading slash.
* @param array $options
- * An array of options such as would be passed to the generator's
- * generateFromRoute() method.
+ * (optional) An associative array of additional options, with the following
+ * elements:
+ * - 'query': An array of query key/value-pairs (without any URL-encoding)
+ * to append to the URL.
+ * - 'fragment': A fragment identifier (named anchor) to append to the URL.
+ * Do not include the leading '#' character.
+ * - 'absolute': Defaults to FALSE. Whether to force the output to be an
+ * absolute link (beginning with http:). Useful for links that will be
+ * displayed outside the site, such as in an RSS feed.
+ * - 'language': An optional language object used to look up the alias
+ * for the URL. If $options['language'] is omitted, it defaults to the
+ * current language for the language type LanguageInterface::TYPE_URL.
+ * - 'https': Whether this URL should point to a secure location. If not
+ * defined, the current scheme is used, so the user stays on HTTP or HTTPS
+ * respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
+ * - 'base_url': Only used internally by a path processor, for example, to
+ * modify the base URL when a language dependent URL requires so.
+ * - 'prefix': Only used internally, to modify the path when a language
+ * dependent URL requires so.
+ * - 'route': The route object for the given path. It will be set by
+ * \Drupal\Core\Routing\UrlGenerator::generateFromRoute().
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index f3d1275..62ece64 100644
--- a/core/lib/Drupal/Core/Routing/UrlGenerator.php
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -309,6 +309,9 @@ class UrlGenerator implements UrlGeneratorInterface {
$name = $this->getRouteDebugMessage($name);
$this->processRoute($name, $route, $parameters, $generated_url);
$path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
+ // Outbound path processors might need the route object for the path, e.g.
+ // to get the path pattern.
+ $options['route'] = $route;
$path = $this->processPath($path, $options, $generated_url);
if (!empty($options['prefix'])) {
diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install
index 0bfe414..d44b2d1 100644
--- a/core/modules/content_translation/content_translation.install
+++ b/core/modules/content_translation/content_translation.install
@@ -29,3 +29,19 @@ function content_translation_enable() {
$message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
}
+
+/**
+ * @addtogroup updates-8.0.0-rc
+ * @{
+ */
+
+/**
+ * Rebuild the routes as the content translation routes have now new names.
+ */
+function content_translation_update_8001() {
+ \Drupal::service('router.builder')->rebuild();
+}
+
+/**
+ * @} End of "addtogroup updates-8.0.0-rc".
+ */
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index ff1b34d..b354e95 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -53,7 +53,7 @@ function content_translation_help($route_name, RouteMatchInterface $route_match)
*/
function content_translation_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
- // Move some of our hook implementations to the end of the list.
+ // Move our hook_entity_type_alter() implementation to the end of the list.
case 'entity_type_alter':
$group = $implementations['content_translation'];
unset($implementations['content_translation']);
@@ -140,7 +140,11 @@ function content_translation_entity_type_alter(array &$entity_types) {
if ($entity_type->hasLinkTemplate('canonical')) {
// Provide default route names for the translation paths.
if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
- $entity_type->setLinkTemplate('drupal:content-translation-overview', $entity_type->getLinkTemplate('canonical') . '/translations');
+ $translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
+ $entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
+ $entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
+ $entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
+ $entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
}
// @todo Remove this as soon as menu access checks rely on the
// controller. See https://www.drupal.org/node/2155787.
diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index d7eb42a..1b8d77d 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -652,7 +652,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$source = $form_state->getValue(array('source_langcode', 'source'));
$entity_type_id = $entity->getEntityTypeId();
- $form_state->setRedirect('content_translation.translation_add_' . $entity_type_id, array(
+ $form_state->setRedirect("entity.$entity_type_id.content_translation_add", array(
$entity_type_id => $entity->id(),
'source' => $source,
'target' => $form_object->getFormLangcode($form_state),
@@ -689,7 +689,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$form_state->setRedirectUrl($entity->urlInfo('delete-form'));
}
else {
- $form_state->setRedirect('content_translation.translation_delete_' . $entity_type_id, [
+ $form_state->setRedirect("entity.$entity_type_id.content_translation_delete", [
$entity_type_id => $entity->id(),
'language' => $form_object->getFormLangcode($form_state),
]);
diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php
index 9c6879a..8c19de1 100644
--- a/core/modules/content_translation/src/Controller/ContentTranslationController.php
+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -127,7 +127,7 @@ class ContentTranslationController extends ControllerBase {
$langcode = $language->getId();
$add_url = new Url(
- 'content_translation.translation_add_' . $entity_type_id,
+ "entity.$entity_type_id.content_translation_add",
array(
'source' => $original,
'target' => $language->getId(),
@@ -138,7 +138,7 @@ class ContentTranslationController extends ControllerBase {
)
);
$edit_url = new Url(
- 'content_translation.translation_edit_' . $entity_type_id,
+ "entity.$entity_type_id.content_translation_edit",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
@@ -148,7 +148,7 @@ class ContentTranslationController extends ControllerBase {
)
);
$delete_url = new Url(
- 'content_translation.translation_delete_' . $entity_type_id,
+ "entity.$entity_type_id.content_translation_delete",
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
index a2b54a8..ebdf562 100644
--- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
+++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
@@ -112,7 +112,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
- $collection->add("content_translation.translation_add_$entity_type_id", $route);
+ $collection->add("entity.$entity_type_id.content_translation_add", $route);
$route = new Route(
$path . '/edit/{language}',
@@ -137,7 +137,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
- $collection->add("content_translation.translation_edit_$entity_type_id", $route);
+ $collection->add("entity.$entity_type_id.content_translation_edit", $route);
$route = new Route(
$path . '/delete/{language}',
@@ -162,7 +162,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
'_admin_route' => $is_admin,
)
);
- $collection->add("content_translation.translation_delete_$entity_type_id", $route);
+ $collection->add("entity.$entity_type_id.content_translation_delete", $route);
}
}
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
index 33d936a..1b79d13 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
@@ -107,7 +107,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = $this->getNewEntityValues($langcode);
- $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
+ $entity_type_id = $entity->getEntityTypeId();
+ $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
@@ -167,7 +168,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$language = ConfigurableLanguage::load($langcode);
$source_langcode = 'it';
$edit = array('source_langcode[source]' => $source_langcode);
- $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
+ $entity_type_id = $entity->getEntityTypeId();
+ $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
@@ -180,7 +182,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
// Add another translation and mark the other ones as outdated.
$values[$langcode] = $this->getNewEntityValues($langcode);
$edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
- $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
+ $entity_type_id = $entity->getEntityTypeId();
+ $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
@@ -207,13 +210,15 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
*/
protected function doTestTranslationOverview() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
- $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
+ $translate_url = $entity->urlInfo('drupal:content-translation-overview');
+ $this->drupalGet($translate_url);
+ $translate_url->setAbsolute(FALSE);
foreach ($this->langcodes as $langcode) {
if ($entity->hasTranslation($langcode)) {
$language = new Language(array('id' => $langcode));
- $view_path = $entity->url('canonical', array('language' => $language));
- $elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path));
+ $view_url = $entity->url('canonical', ['language' => $language]);
+ $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]);
$this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode)));
$edit_path = $entity->url('edit-form', array('language' => $language));
$elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path));
@@ -343,7 +348,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
// Check that the translator cannot delete the original translation.
$args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
- $this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args));
+ $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
$this->assertResponse(403);
}
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php b/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php
index 9d8a052..63c1963 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php
@@ -73,7 +73,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Create a translation.
$this->drupalLogin($this->translator);
- $add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
+ $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$this->drupalPostForm($add_translation_url, array(), t('Save'));
$this->rebuildContainer();
}
@@ -175,7 +175,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
$this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args));
// Check whether the user is allowed to create a translation.
- $add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
+ $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
if ($expected_status['add_translation'] == 200) {
$this->clickLink('Add');
$this->assertUrl($add_translation_url->toString(), [], 'The translation overview points to the translation form when creating translations.');
@@ -193,7 +193,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Check whether the user is allowed to edit a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
- $edit_translation_url = Url::fromRoute('content_translation.translation_edit_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
+ $edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['edit_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['edit'] == 200;
@@ -221,7 +221,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Check whether the user is allowed to delete a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
- $delete_translation_url = Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
+ $delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
if ($expected_status['delete_translation'] == 200) {
$this->drupalGet($translations_url);
$editor = $expected_status['delete'] == 200;
diff --git a/core/modules/language/language.install b/core/modules/language/language.install
new file mode 100644
index 0000000..dcec16a
--- /dev/null
+++ b/core/modules/language/language.install
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Update functions for Language module.
+ */
+
+/**
+ * Rebuild the container as services changed.
+ */
+function language_update_8001() {
+ \Drupal::service('kernel')->invalidateContainer();
+}
diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml
index fc9bba1..19d0f21 100644
--- a/core/modules/language/language.services.yml
+++ b/core/modules/language/language.services.yml
@@ -9,7 +9,7 @@ services:
- [initLanguageManager]
language.config_subscriber:
class: Drupal\language\EventSubscriber\ConfigSubscriber
- arguments: ['@language_manager', '@language.default', '@config.factory']
+ arguments: ['@language_manager', '@language.default', '@config.factory', '@language_negotiator']
tags:
- { name: event_subscriber }
language.config_factory_override:
diff --git a/core/modules/language/src/EventSubscriber/ConfigSubscriber.php b/core/modules/language/src/EventSubscriber/ConfigSubscriber.php
index e1875ce..f1c7582 100644
--- a/core/modules/language/src/EventSubscriber/ConfigSubscriber.php
+++ b/core/modules/language/src/EventSubscriber/ConfigSubscriber.php
@@ -14,6 +14,8 @@ use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\language\ConfigurableLanguageManager;
+use Drupal\language\HttpKernel\PathProcessorLanguage;
+use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@@ -43,6 +45,20 @@ class ConfigSubscriber implements EventSubscriberInterface {
protected $configFactory;
/**
+ * The language negotiator.
+ *
+ * @var \Drupal\language\LanguageNegotiatorInterface
+ */
+ protected $languageNegotiator;
+
+ /**
+ * The language path processor.
+ *
+ * @var \Drupal\language\HttpKernel\PathProcessorLanguage
+ */
+ protected $pathProcessorLanguage;
+
+ /**
* Constructs a new class object.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -51,11 +67,14 @@ class ConfigSubscriber implements EventSubscriberInterface {
* The default language.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
+ * @param \Drupal\language\LanguageNegotiatorInterface $language_negotiator
+ * The language negotiator.
*/
- public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory) {
+ public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory, LanguageNegotiatorInterface $language_negotiator) {
$this->languageManager = $language_manager;
$this->languageDefault = $language_default;
$this->configFactory = $config_factory;
+ $this->languageNegotiator = $language_negotiator;
}
/**
@@ -102,6 +121,25 @@ class ConfigSubscriber implements EventSubscriberInterface {
// Trigger a container rebuild on the next request by invalidating it.
ConfigurableLanguageManager::rebuildServices();
}
+ elseif ($saved_config->getName() == 'language.types' && $event->isChanged('negotiation')) {
+ // If the negotiation configuration changed the language negotiator and
+ // the language path processor have to be reset so that they regenerate
+ // the method instances and also sort them accordingly to the new config.
+ $this->languageNegotiator->reset();
+ if (isset($this->pathProcessorLanguage)) {
+ $this->pathProcessorLanguage->reset();
+ }
+ }
+ }
+
+ /**
+ * Injects the language path processors on multilingual site configuration.
+ *
+ * @param \Drupal\language\HttpKernel\PathProcessorLanguage $path_processor_language
+ * The language path processor.
+ */
+ public function setPathProcessorLanguage(PathProcessorLanguage $path_processor_language) {
+ $this->pathProcessorLanguage = $path_processor_language;
}
/**
diff --git a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
index caddfc0..aa58709 100644
--- a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
+++ b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
@@ -13,6 +13,7 @@ use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\language\ConfigurableLanguageManagerInterface;
+use Drupal\language\EventSubscriber\ConfigSubscriber;
use Drupal\language\LanguageNegotiatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Session\AccountInterface;
@@ -58,6 +59,14 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
protected $multilingual;
/**
+ * The language configuration event subscriber.
+ *
+ * @var \Drupal\language\EventSubscriber\ConfigSubscriber
+ */
+ protected $configSubscriber;
+
+
+ /**
* Constructs a PathProcessorLanguage object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
@@ -68,12 +77,15 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
* The language negotiator.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current active user.
+ * @param \Drupal\language\EventSubscriber\ConfigSubscriber $config_subscriber
+ * The language configuration event subscriber.
*/
- public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user) {
+ public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user, ConfigSubscriber $config_subscriber) {
$this->config = $config;
$this->languageManager = $language_manager;
$this->negotiator = $negotiator;
$this->negotiator->setCurrentUser($current_user);
+ $this->configSubscriber = $config_subscriber;
}
/**
@@ -152,4 +164,22 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa
});
}
+ /**
+ * Initializes the injected event subscriber with the language path processor.
+ *
+ * The language path processor service is registered only on multilingual
+ * site configuration, thus we inject it in the event subscriber only when
+ * it is initialized.
+ */
+ public function initConfigSubscriber() {
+ $this->configSubscriber->setPathProcessorLanguage($this);
+ }
+
+ /**
+ * Resets the collected processors instances.
+ */
+ public function reset() {
+ $this->processors = array();
+ }
+
}
diff --git a/core/modules/language/src/LanguageServiceProvider.php b/core/modules/language/src/LanguageServiceProvider.php
index 2f1840c..c3f1cd1 100644
--- a/core/modules/language/src/LanguageServiceProvider.php
+++ b/core/modules/language/src/LanguageServiceProvider.php
@@ -39,7 +39,9 @@ class LanguageServiceProvider extends ServiceProviderBase {
->addArgument(new Reference('config.factory'))
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('language_negotiator'))
- ->addArgument(new Reference('current_user'));
+ ->addArgument(new Reference('current_user'))
+ ->addArgument(new Reference('language.config_subscriber'))
+ ->addMethodCall('initConfigSubscriber');
}
}
diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php
new file mode 100644
index 0000000..4ce022d
--- /dev/null
+++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php
@@ -0,0 +1,294 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity.
+ */
+
+namespace Drupal\language\Plugin\LanguageNegotiation;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Url;
+use Drupal\language\LanguageNegotiationMethodBase;
+use Drupal\language\LanguageSwitcherInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Class for identifying the content translation language.
+ *
+ * @LanguageNegotiation(
+ * id = Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::METHOD_ID,
+ * types = {Drupal\Core\Language\LanguageInterface::TYPE_CONTENT},
+ * weight = -9,
+ * name = @Translation("Content language"),
+ * description = @Translation("Determines the content language from a request parameter."),
+ * )
+ */
+class LanguageNegotiationContentEntity extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface, ContainerFactoryPluginInterface {
+
+ /**
+ * The language negotiation method ID.
+ */
+ const METHOD_ID = 'language-content-entity';
+
+ /**
+ * The query string parameter.
+ */
+ const QUERY_PARAMETER = 'language_content_entity';
+
+ /**
+ * A list of all the link paths of enabled content entities.
+ *
+ * @var array
+ */
+ protected $contentEntityPaths;
+
+ /**
+ * Static cache for the language negotiation order check.
+ *
+ * @see \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::hasLowerLanguageNegotiationWeight()
+ *
+ * @var bool
+ */
+ protected $hasLowerLanguageNegotiationWeightResult;
+
+ /**
+ * Static cache of outbound route paths per request.
+ *
+ * @var \SplObjectStorage
+ */
+ protected $paths;
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface
+ */
+ protected $entityManager;
+
+ /**
+ * Constructs a new LanguageNegotiationContentEntity instance.
+ *
+ * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+ * The entity manager.
+ */
+ public function __construct(EntityManagerInterface $entity_manager) {
+ $this->entityManager = $entity_manager;
+ $this->paths = new \SplObjectStorage();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static($container->get('entity.manager'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLangcode(Request $request = NULL) {
+ $langcode = $request->get(static::QUERY_PARAMETER);
+
+ $language_enabled = array_key_exists($langcode, $this->languageManager->getLanguages());
+ return $language_enabled ? $langcode : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
+ // If appropriate, process outbound to add a query parameter to the url and
+ // remove the language option, so that url negotiator does not rewrite the
+ // url.
+
+ // First, check if processing conditions are met.
+ if (!($request && !empty($options['route']) && $this->hasLowerLanguageNegotiationWeight() && $this->meetsContentEntityRoutesCondition($options['route'], $request))) {
+ return $path;
+ }
+
+ if (isset($options['language']) || $langcode = $this->getLangcode($request)) {
+ // If the language option is set, unset it, so that the url language
+ // negotiator does not rewrite the url.
+ if (isset($options['language'])) {
+ $langcode = $options['language']->getId();
+ unset($options['language']);
+ }
+
+ if (isset($options['query']) && is_string($options['query'])) {
+ $query = [];
+ parse_str($options['query'], $query);
+ $options['query'] = $query;
+ }
+ else {
+ $options['query'] = [];
+ }
+
+ if (!isset($options['query'][static::QUERY_PARAMETER])) {
+ $query_addon = [static::QUERY_PARAMETER => $langcode];
+ $options['query'] += $query_addon;
+ // @todo Remove this once https://www.drupal.org/node/2507005 lands.
+ $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($query_addon);
+ }
+
+ if ($bubbleable_metadata) {
+ // Cached URLs that have been processed by this outbound path
+ // processor must be:
+ $bubbleable_metadata
+ // - varied by the content language query parameter.
+ ->addCacheContexts(['url.query_args:' . static::QUERY_PARAMETER]);
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLanguageSwitchLinks(Request $request, $type, Url $url) {
+ $links = [];
+ $query = [];
+ parse_str($request->getQueryString(), $query);
+
+ foreach ($this->languageManager->getNativeLanguages() as $language) {
+ $langcode = $language->getId();
+ $query[static::QUERY_PARAMETER] = $langcode;
+ $links[$langcode] = [
+ 'url' => $url,
+ 'title' => $language->getName(),
+ 'attributes' => ['class' => ['language-link']],
+ 'query' => $query,
+ ];
+ }
+
+ return $links;
+ }
+
+ /**
+ * Determines if content entity language negotiator has higher priority.
+ *
+ * The content entity language negotiator having higher priority than the url
+ * language negotiator, is a criteria in
+ * \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::processOutbound().
+ *
+ * @return bool
+ * TRUE if the the content entity language negotiator has higher priority
+ * than the url language negotiator, FALSE otherwise.
+ */
+ protected function hasLowerLanguageNegotiationWeight() {
+ if (!isset($this->hasLowerLanguageNegotiationWeightResult)) {
+ // Only run if the LanguageNegotiationContentEntity outbound function is
+ // being executed before the outbound function of LanguageNegotiationUrl.
+ $content_method_weights = $this->config->get('language.types')->get('negotiation.language_content.enabled') ?: [];
+
+ // Check if the content language is configured to be dependent on the
+ // url negotiator directly or indirectly over the interface negotiator.
+ if (isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) && ($content_method_weights[static::METHOD_ID] > $content_method_weights[LanguageNegotiationUrl::METHOD_ID])) {
+ $this->hasLowerLanguageNegotiationWeightResult = FALSE;
+ }
+ else {
+ $check_interface_method = FALSE;
+ if (isset($content_method_weights[LanguageNegotiationUI::METHOD_ID])) {
+ $interface_method_weights = $this->config->get('language.types')->get('negotiation.language_interface.enabled') ?: [];
+ $check_interface_method = isset($interface_method_weights[LanguageNegotiationUrl::METHOD_ID]);
+ }
+ if ($check_interface_method) {
+ $max_weight = $content_method_weights[LanguageNegotiationUI::METHOD_ID];
+ $max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? max($max_weight, $content_method_weights[LanguageNegotiationUrl::METHOD_ID]) : $max_weight;
+ }
+ else {
+ $max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? $content_method_weights[LanguageNegotiationUrl::METHOD_ID] : PHP_INT_MAX;
+ }
+
+ $this->hasLowerLanguageNegotiationWeightResult = $content_method_weights[static::METHOD_ID] < $max_weight;
+ }
+ }
+
+ return $this->hasLowerLanguageNegotiationWeightResult;
+ }
+
+ /**
+ * Determines if content entity route condition is met.
+ *
+ * Requirements: currently being on an content entity route and processing
+ * outbound url pointing to the same content entity.
+ *
+ * @param \Symfony\Component\Routing\Route $outbound_route
+ * The route object for the current outbound url being processed.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The HttpRequest object representing the current request.
+ *
+ * @return bool
+ * TRUE if the content entity route condition is met, FALSE otherwise.
+ */
+ protected function meetsContentEntityRoutesCondition(Route $outbound_route, Request $request) {
+ $outbound_path_pattern = $outbound_route->getPath();
+ $storage = isset($this->paths[$request]) ? $this->paths[$request] : [];
+ if (!isset($storage[$outbound_path_pattern])) {
+ $storage[$outbound_path_pattern] = FALSE;
+
+ // Check if the outbound route points to the current entity.
+ if ($content_entity_type_id_for_current_route = $this->getContentEntityTypeIdForCurrentRequest($request)) {
+ if (!empty($this->getContentEntityPaths()[$outbound_path_pattern]) && $content_entity_type_id_for_current_route == $this->getContentEntityPaths()[$outbound_path_pattern]) {
+ $storage[$outbound_path_pattern] = TRUE;
+ }
+ }
+
+ $this->paths[$request] = $storage;
+ }
+
+ return $storage[$outbound_path_pattern];
+ }
+
+ /**
+ * Returns the content entity type ID from the current request for the route.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The HttpRequest object representing the current request.
+ *
+ * @return string
+ * The entity type ID for the route from the request.
+ */
+ protected function getContentEntityTypeIdForCurrentRequest(Request $request) {
+ $content_entity_type_id_for_current_route = '';
+
+ if ($current_route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
+ $current_route_path = $current_route->getPath();
+ $content_entity_type_id_for_current_route = isset($this->getContentEntityPaths()[$current_route_path]) ? $this->getContentEntityPaths()[$current_route_path] : '';
+ }
+
+ return $content_entity_type_id_for_current_route;
+ }
+
+ /**
+ * Returns the paths for the link templates of all content entities.
+ *
+ * @return array
+ * An array of all content entity type IDs, keyed by the corresponding link
+ * template paths.
+ */
+ protected function getContentEntityPaths() {
+ if (!isset($this->contentEntityPaths)) {
+ $this->contentEntityPaths = [];
+ $entity_types = $this->entityManager->getDefinitions();
+ foreach ($entity_types as $entity_type_id => $entity_type) {
+ if ($entity_type->isSubclassOf(ContentEntityInterface::class)) {
+ $entity_paths = array_fill_keys($entity_type->getLinkTemplates(), $entity_type_id);
+ $this->contentEntityPaths = array_merge($this->contentEntityPaths, $entity_paths);
+ }
+ }
+ }
+
+ return $this->contentEntityPaths;
+ }
+
+}
diff --git a/core/modules/language/src/Tests/EntityUrlLanguageTest.php b/core/modules/language/src/Tests/EntityUrlLanguageTest.php
index 915f7f6..f172b38 100644
--- a/core/modules/language/src/Tests/EntityUrlLanguageTest.php
+++ b/core/modules/language/src/Tests/EntityUrlLanguageTest.php
@@ -7,22 +7,34 @@
namespace Drupal\language\Tests;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\language\Entity\ConfigurableLanguage;
-use Drupal\simpletest\KernelTestBase;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
/**
* Tests the language of entity URLs.
* @group language
*/
-class EntityUrlLanguageTest extends KernelTestBase {
+class EntityUrlLanguageTest extends LanguageTestBase {
/**
* Modules to enable.
*
* @var array
*/
- public static $modules = ['language', 'entity_test', 'user', 'system'];
+ public static $modules = ['entity_test', 'user'];
+
+ /**
+ * The entity being used for testing.
+ *
+ * @var \Drupal\Core\Entity\ContentEntityInterface
+ */
+ protected $entity;
protected function setUp() {
parent::setUp();
@@ -37,33 +49,93 @@ class EntityUrlLanguageTest extends KernelTestBase {
ConfigurableLanguage::create(['id' => 'es'])->save();
ConfigurableLanguage::create(['id' => 'fr'])->save();
- $this->config('language.types')->setData([
- 'configurable' => ['language_interface'],
- 'negotiation' => ['language_interface' => ['enabled' => ['language-url' => 0]]],
- ])->save();
- $this->config('language.negotiation')->setData([
- 'url' => [
- 'source' => 'path_prefix',
- 'prefixes' => ['en' => 'en', 'es' => 'es', 'fr' => 'fr']
- ],
- ])->save();
+ $config = $this->config('language.negotiation');
+ $config->set('url.prefixes', ['en' => 'en', 'es' => 'es', 'fr' => 'fr'])
+ ->save();
$this->kernel->rebuildContainer();
- $this->container = $this->kernel->getContainer();
- \Drupal::setContainer($this->container);
+
+ $this->createTranslatableEntity();
}
/**
* Ensures that entity URLs in a language have the right language prefix.
*/
public function testEntityUrlLanguage() {
- $entity = EntityTest::create();
- $entity->addTranslation('es', ['name' => 'name spanish']);
- $entity->addTranslation('fr', ['name' => 'name french']);
- $entity->save();
-
- $this->assertTrue(strpos($entity->urlInfo()->toString(), '/en/entity_test/' . $entity->id()) !== FALSE);
- $this->assertTrue(strpos($entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $entity->id()) !== FALSE);
- $this->assertTrue(strpos($entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $entity->id()) !== FALSE);
+ $this->assertTrue(strpos($this->entity->urlInfo()->toString(), '/en/entity_test/' . $this->entity->id()) !== FALSE);
+ $this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $this->entity->id()) !== FALSE);
+ $this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $this->entity->id()) !== FALSE);
+ }
+
+ /**
+ * Ensures correct entity URLs with the method language-content-entity enabled.
+ *
+ * Test case with the method language-content-entity enabled and configured
+ * with higher and also with lower priority than the method language-url.
+ */
+ public function testEntityUrlLanguageWithLanguageContentEnabled() {
+ // Define the method language-content-entity with a higher priority than
+ // language-url.
+ $config = $this->config('language.types');
+ $config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]);
+ $config->set('negotiation.language_content.enabled', [
+ LanguageNegotiationContentEntity::METHOD_ID => 0,
+ LanguageNegotiationUrl::METHOD_ID => 1
+ ]);
+ $config->save();
+
+ // Without being on an content entity route the default entity URL tests
+ // should still pass.
+ $this->testEntityUrlLanguage();
+
+ // Now switching to an entity route, so that the URL links are generated
+ // while being on an entity route.
+ $this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical');
+
+ // The method language-content-entity should run before language-url and
+ // append query parameter for the content language and prevent language-url
+ // from overwriting the url.
+ $this->assertTrue(strpos($this->entity->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=en') !== FALSE);
+ $this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=es') !== FALSE);
+ $this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=fr') !== FALSE);
+
+ // Define the method language-url with a higher priority than
+ // language-content-entity. This configuration should match the default one,
+ // where the language-content-entity is turned off.
+ $config->set('negotiation.language_content.enabled', [
+ LanguageNegotiationUrl::METHOD_ID => 0,
+ LanguageNegotiationContentEntity::METHOD_ID => 1
+ ]);
+ $config->save();
+
+ // The default entity URL tests should pass again with the current
+ // configuration.
+ $this->testEntityUrlLanguage();
+ }
+
+ /**
+ * Creates a translated entity.
+ */
+ protected function createTranslatableEntity() {
+ $this->entity = EntityTest::create();
+ $this->entity->addTranslation('es', ['name' => 'name spanish']);
+ $this->entity->addTranslation('fr', ['name' => 'name french']);
+ $this->entity->save();
+ }
+
+ /**
+ * Sets the current request to a specific path with the corresponding route.
+ *
+ * @param string $path
+ * The path for which the current request should be created.
+ * @param string $route_name
+ * The route name for which the route object for the request should be
+ * created.
+ */
+ protected function setCurrentRequestForRoute($path, $route_name) {
+ $request = Request::create($path);
+ $request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
+ $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path));
+ $this->container->get('request_stack')->push($request);
}
}
diff --git a/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php b/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php
new file mode 100644
index 0000000..5373096
--- /dev/null
+++ b/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Tests\LanguageNegotiationLanguageContentEntityTest.
+ */
+
+namespace Drupal\language\Tests;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
+use Drupal\simpletest\WebTestBase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests language negotiation with the language negotiator content entity.
+ *
+ * @group language
+ */
+class LanguageNegotiationContentEntityTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'language_test', 'entity_test', 'system');
+
+ /**
+ * The entity being used for testing.
+ *
+ * @var \Drupal\Core\Entity\ContentEntityInterface
+ */
+ protected $entity;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ ConfigurableLanguage::create(['id' => 'es'])->save();
+ ConfigurableLanguage::create(['id' => 'fr'])->save();
+
+ // In order to reflect the changes for a multilingual site in the container
+ // we have to rebuild it.
+ $this->rebuildContainer();
+
+ $this->createTranslatableEntity();
+
+ $user = $this->drupalCreateUser(array('view test entity'));
+ $this->drupalLogin($user);
+ }
+
+ /**
+ * Tests default with content language remaining same as interface language.
+ */
+ public function testDefaultConfiguration() {
+ $translation = $this->entity;
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
+
+ $translation = $this->entity->getTranslation('es');
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
+
+ $translation = $this->entity->getTranslation('fr');
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()]));
+ }
+
+ /**
+ * Tests enabling the language negotiator language_content_entity.
+ */
+ public function testEnabledLanguageContentNegotiator() {
+ // Define the method language-url with a higher priority than
+ // language-content-entity. This configuration should match the default one,
+ // where the language-content-entity is turned off.
+ $config = $this->config('language.types');
+ $config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]);
+ $config->set('negotiation.language_content.enabled', [
+ LanguageNegotiationUrl::METHOD_ID => 0,
+ LanguageNegotiationContentEntity::METHOD_ID => 1
+ ]);
+ $config->save();
+
+ // In order to reflect the changes for a multilingual site in the container
+ // we have to rebuild it.
+ $this->rebuildContainer();
+
+ // The tests for the default configuration should still pass.
+ $this->testDefaultConfiguration();
+
+ // Define the method language-content-entity with a higher priority than
+ // language-url.
+ $config->set('negotiation.language_content.enabled', [
+ LanguageNegotiationContentEntity::METHOD_ID => 0,
+ LanguageNegotiationUrl::METHOD_ID => 1
+ ]);
+ $config->save();
+
+ // In order to reflect the changes for a multilingual site in the container
+ // we have to rebuild it.
+ $this->rebuildContainer();
+
+ // The method language-content-entity should run before language-url and
+ // append query parameter for the content language and prevent language-url
+ // from overwriting the URL.
+ $default_site_langcode = $this->config('system.site')->get('default_langcode');
+
+ // Now switching to an entity route, so that the URL links are generated
+ // while being on an entity route.
+ $this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical');
+
+ $translation = $this->entity;
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue(($last_interface_language == $default_site_langcode) && ($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), 'Interface language and Content language are the same as the default translation language of the entity.');
+ $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
+ $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
+
+ $translation = $this->entity->getTranslation('es');
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
+ $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
+
+ $translation = $this->entity->getTranslation('fr');
+ $this->drupalGet($translation->urlInfo());
+ $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+ $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
+ $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
+ $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.');
+ $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.');
+ }
+
+ /**
+ * Creates a translated entity.
+ */
+ protected function createTranslatableEntity() {
+ $this->entity = EntityTest::create();
+ $this->entity->addTranslation('es', ['name' => 'name spanish']);
+ $this->entity->addTranslation('fr', ['name' => 'name french']);
+ $this->entity->save();
+ }
+
+ /**
+ * Sets the current request to a specific path with the corresponding route.
+ *
+ * @param string $path
+ * The path for which the current request should be created.
+ * @param string $route_name
+ * The route name for which the route object for the request should be
+ * created.
+ */
+ protected function setCurrentRequestForRoute($path, $route_name) {
+ $request = Request::create($path);
+ $request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
+ $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path));
+ $this->container->get('request_stack')->push($request);
+ }
+
+}
diff --git a/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php
index 4645d68..16ba13e 100644
--- a/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php
+++ b/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php
@@ -221,8 +221,13 @@ class LanguageUILanguageNegotiationTest extends WebTestBase {
// Unknown language prefix should return 404.
$definitions = \Drupal::languageManager()->getNegotiator()->getNegotiationMethods();
+ // Enable only methods, which are either not limited to a specific language
+ // type or are supporting the interface language type.
+ $language_interface_method_definitions = array_filter($definitions, function ($method_definition) {
+ return !isset($method_definition['types']) || (isset($method_definition['types']) && in_array(LanguageInterface::TYPE_INTERFACE, $method_definition['types']));
+ });
$this->config('language.types')
- ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($definitions)))
+ ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($language_interface_method_definitions)))
->save();
$this->drupalGet("$langcode_unknown/admin/config", array(), $http_header_browser_fallback);
$this->assertResponse(404, "Unknown language path prefix should return 404");
diff --git a/core/modules/node/src/Tests/NodeTranslationUITest.php b/core/modules/node/src/Tests/NodeTranslationUITest.php
index b42eb76..0bb81c0 100644
--- a/core/modules/node/src/Tests/NodeTranslationUITest.php
+++ b/core/modules/node/src/Tests/NodeTranslationUITest.php
@@ -94,7 +94,8 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = array('title' => array(array('value' => $this->randomMachineName())));
- $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
+ $entity_type_id = $entity->getEntityTypeId();
+ $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
diff --git a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
index 93766ef..fa14999 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
@@ -30,7 +30,6 @@ class UpdatePathRC1TestBaseFilledTest extends UpdatePathRC1TestBaseTest {
* Tests that the content and configuration were properly updated.
*/
public function testUpdatedSite() {
- // @todo there are no updates to run.
$this->runUpdates();
$spanish = \Drupal::languageManager()->getLanguage('es');
diff --git a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
index d01a1f6..da61f5f 100644
--- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
+++ b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
@@ -150,11 +150,16 @@ class PathProcessorTest extends UnitTestCase {
$current_user = $this->getMockBuilder('Drupal\Core\Session\AccountInterface')
->getMock();
+ // Create a config event subscriber stub.
+ $config_subscriber = $this->getMockBuilder('Drupal\language\EventSubscriber\ConfigSubscriber')
+ ->disableOriginalConstructor()
+ ->getMock();
+
// Create the processors.
$alias_processor = new PathProcessorAlias($alias_manager);
$decode_processor = new PathProcessorDecode();
$front_processor = new PathProcessorFront($config_factory_stub);
- $language_processor = new PathProcessorLanguage($config_factory_stub, $this->languageManager, $negotiator, $current_user);
+ $language_processor = new PathProcessorLanguage($config_factory_stub, $this->languageManager, $negotiator, $current_user, $config_subscriber);
// First, test the processor manager with the processors in the incorrect
// order. The alias processor will run before the language processor, meaning