diff --git a/core/modules/config_translation/src/Access/ConfigTranslationFormAccess.php b/core/modules/config_translation/src/Access/ConfigTranslationFormAccess.php index 4e041ce..cc48061 100644 --- a/core/modules/config_translation/src/Access/ConfigTranslationFormAccess.php +++ b/core/modules/config_translation/src/Access/ConfigTranslationFormAccess.php @@ -2,6 +2,7 @@ namespace Drupal\config_translation\Access; +use Drupal\config_translation\Exception\ConfigMapperLanguageException; use Drupal\Core\Access\AccessResult; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; @@ -19,18 +20,29 @@ public function access(RouteMatchInterface $route_match, AccountInterface $accou // checks in addition to the checks performed for the translation overview. $base_access = parent::access($route_match, $account); if ($base_access->isAllowed()) { - $target_language = $this->languageManager->getLanguage($langcode); - - // Make sure that the target language is not locked, and that the target - // language is not the original submission language. Although technically - // configuration can be overlaid with translations in the same language, - // that is logically not a good idea. - $access = - !empty($target_language) && - !$target_language->isLocked() && - (empty($this->sourceLanguage) || ($target_language->getId() != $this->sourceLanguage->getId())); - - return $base_access->andIf(AccessResult::allowedIf($access)); + try { + // We do not use the result value, but we need to check that the mapper + // only has a single language code. + $this->mapper->getLangcode(); + + $target_language = $this->languageManager->getLanguage($langcode); + + // Make sure that the target language is not locked, and that the target + // language is not the original submission language. Although + // technically configuration can be overlaid with translations in the + // same language, that is logically not a good idea. + $access = + !empty($target_language) && + !$target_language->isLocked() && + (empty($this->sourceLanguage) || ($target_language->getId() != $this->sourceLanguage->getId())); + + return $base_access->andIf(AccessResult::allowedIf($access)); + } + catch (ConfigMapperLanguageException $exception) { + // In contrast to ConfigTranslationOverviewAccess this does not grant + // access to a mapper if the languages do not match. + return AccessResult::forbidden()->inheritCacheability($base_access); + } } return $base_access; } diff --git a/core/modules/config_translation/src/Access/ConfigTranslationOverviewAccess.php b/core/modules/config_translation/src/Access/ConfigTranslationOverviewAccess.php index 91ccf49..1fa4fbc 100644 --- a/core/modules/config_translation/src/Access/ConfigTranslationOverviewAccess.php +++ b/core/modules/config_translation/src/Access/ConfigTranslationOverviewAccess.php @@ -2,6 +2,7 @@ namespace Drupal\config_translation\Access; +use Drupal\config_translation\Exception\ConfigMapperLanguageException; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\config_translation\ConfigMapperManagerInterface; use Drupal\Core\Access\AccessResult; @@ -29,6 +30,13 @@ class ConfigTranslationOverviewAccess implements AccessInterface { protected $languageManager; /** + * The configuration mapper to check access for. + * + * @var \Drupal\config_translation\ConfigMapperInterface + */ + protected $mapper; + + /** * The source language. * * @var \Drupal\Core\Language\LanguageInterface @@ -62,10 +70,19 @@ public function __construct(ConfigMapperManagerInterface $config_mapper_manager, public function access(RouteMatchInterface $route_match, AccountInterface $account) { $route = $route_match->getRouteObject(); - /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */ - $mapper = $this->configMapperManager->createInstance($route->getDefault('plugin_id')); - $mapper->populateFromRouteMatch($route_match); - $this->sourceLanguage = $this->languageManager->getLanguage($mapper->getLangcode()); + $this->mapper = $this->configMapperManager->createInstance($route->getDefault('plugin_id')); + $this->mapper->populateFromRouteMatch($route_match); + + try { + $langcode = $this->mapper->getLangcode(); + } + catch (ConfigMapperLanguageException $exception) { + // ConfigTranslationController shows a helpful message if the language + // codes do not match, so do not let that prevent granting access. + $langcode = 'en'; + } + + $this->sourceLanguage = $this->languageManager->getLanguage($langcode); // Allow access to the translation overview if the proper permission is // granted, the configuration has translatable pieces, and the source @@ -73,8 +90,8 @@ public function access(RouteMatchInterface $route_match, AccountInterface $accou $source_language_access = is_null($this->sourceLanguage) || !$this->sourceLanguage->isLocked(); $access = $account->hasPermission('translate configuration') && - $mapper->hasSchema() && - $mapper->hasTranslatable() && + $this->mapper->hasSchema() && + $this->mapper->hasTranslatable() && $source_language_access; return AccessResult::allowedIf($access)->cachePerPermissions(); diff --git a/core/modules/config_translation/src/ConfigMapperInterface.php b/core/modules/config_translation/src/ConfigMapperInterface.php index be26d4e..57c1ccd 100644 --- a/core/modules/config_translation/src/ConfigMapperInterface.php +++ b/core/modules/config_translation/src/ConfigMapperInterface.php @@ -203,6 +203,17 @@ public function getConfigData(); public function getLangcode(); /** + * Returns the language code of a configuration object given its name. + * + * @param string $config_name + * The name of the configuration object. + * + * @return string + * The language code of the configuration object. + */ + public function getLangcodeFromConfig($config_name); + + /** * Sets the original language code. * * @param string $langcode diff --git a/core/modules/config_translation/src/ConfigNamesMapper.php b/core/modules/config_translation/src/ConfigNamesMapper.php index 5b6028a..bb82fe5 100644 --- a/core/modules/config_translation/src/ConfigNamesMapper.php +++ b/core/modules/config_translation/src/ConfigNamesMapper.php @@ -2,6 +2,7 @@ namespace Drupal\config_translation; +use Drupal\config_translation\Exception\ConfigMapperLanguageException; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Language\LanguageInterface; @@ -380,17 +381,10 @@ public function getTypeLabel() { * {@inheritdoc} */ public function getLangcode() { - $config_factory = $this->configFactory; - $langcodes = array_map(function($name) use ($config_factory) { - // Default to English if no language code was provided in the file. - // Although it is a best practice to include a language code, if the - // developer did not think about a multilingual use-case, we fall back - // on assuming the file is English. - return $config_factory->get($name)->get('langcode') ?: 'en'; - }, $this->getConfigNames()); + $langcodes = array_map([$this, 'getLangcodeFromConfig'], $this->getConfigNames()); if (count(array_unique($langcodes)) > 1) { - throw new \RuntimeException('A config mapper can only contain configuration for a single language.'); + throw new ConfigMapperLanguageException('A config mapper can only contain configuration for a single language.'); } return reset($langcodes); @@ -399,6 +393,17 @@ public function getLangcode() { /** * {@inheritdoc} */ + public function getLangcodeFromConfig($config_name) { + // Default to English if no language code was provided in the file. + // Although it is a best practice to include a language code, if the + // developer did not think about a multilingual use case, we fall back + // on assuming the file is English. + return $this->configFactory->get($config_name)->get('langcode') ?: 'en'; + } + + /** + * {@inheritdoc} + */ public function setLangcode($langcode) { $this->langcode = $langcode; return $this; diff --git a/core/modules/config_translation/src/Controller/ConfigTranslationController.php b/core/modules/config_translation/src/Controller/ConfigTranslationController.php index 376def0..927a1e2 100644 --- a/core/modules/config_translation/src/Controller/ConfigTranslationController.php +++ b/core/modules/config_translation/src/Controller/ConfigTranslationController.php @@ -3,11 +3,14 @@ namespace Drupal\config_translation\Controller; use Drupal\config_translation\ConfigMapperManagerInterface; +use Drupal\config_translation\Exception\ConfigMapperLanguageException; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; @@ -64,6 +67,13 @@ class ConfigTranslationController extends ControllerBase { protected $languageManager; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs a ConfigTranslationController. * * @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager @@ -78,14 +88,17 @@ class ConfigTranslationController extends ControllerBase { * The current user. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. */ - public function __construct(ConfigMapperManagerInterface $config_mapper_manager, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, AccountInterface $account, LanguageManagerInterface $language_manager) { + public function __construct(ConfigMapperManagerInterface $config_mapper_manager, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, AccountInterface $account, LanguageManagerInterface $language_manager, RendererInterface $renderer) { $this->configMapperManager = $config_mapper_manager; $this->accessManager = $access_manager; $this->router = $router; $this->pathProcessor = $path_processor; $this->account = $account; $this->languageManager = $language_manager; + $this->renderer = $renderer; } /** @@ -98,7 +111,8 @@ public static function create(ContainerInterface $container) { $container->get('router'), $container->get('path_processor_manager'), $container->get('current_user'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('renderer') ); } @@ -127,7 +141,33 @@ public function itemPage(Request $request, RouteMatchInterface $route_match, $pl if (count($languages) == 1) { drupal_set_message($this->t('In order to translate configuration, the website must have at least two languages.', array(':url' => $this->url('entity.configurable_language.collection'))), 'warning'); } - $original_langcode = $mapper->getLangcode(); + + try { + $original_langcode = $mapper->getLangcode(); + $operations_access = TRUE; + } + catch (ConfigMapperLanguageException $exception) { + $items = []; + foreach ($mapper->getConfigNames() as $config_name) { + $langcode = $mapper->getLangcodeFromConfig($config_name); + $items[] = $this->t('@name: @langcode', [ + '@name' => $config_name, + '@langcode' => $langcode, + ]); + } + $message = [ + 'message' => ['#markup' => $this->t('The configuration objects have different language codes so they cannot be translated:')], + 'items' => [ + '#theme' => 'item_list', + '#items' => $items, + ], + ]; + drupal_set_message($this->renderer->renderPlain($message), 'warning'); + + $original_langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; + $operations_access = FALSE; + } + if (!isset($languages[$original_langcode])) { // If the language is not configured on the site, create a dummy language // object for this listing only to ensure the user gets useful info. @@ -205,6 +245,9 @@ public function itemPage(Request $request, RouteMatchInterface $route_match, $pl $page['languages'][$langcode]['operations'] = array( '#type' => 'operations', '#links' => $operations, + // Even if the mapper contains multiple language codes, the source + // configuration can still be edited. + '#access' => ($langcode == $original_langcode) || $operations_access, ); } return $page; diff --git a/core/modules/config_translation/src/Exception/ConfigMapperLanguageException.php b/core/modules/config_translation/src/Exception/ConfigMapperLanguageException.php new file mode 100644 index 0000000..db4d202 --- /dev/null +++ b/core/modules/config_translation/src/Exception/ConfigMapperLanguageException.php @@ -0,0 +1,9 @@ +mapper = $mapper; $this->language = $language; - $this->sourceLanguage = $this->languageManager->getLanguage($this->mapper->getLangcode()); + + // ConfigTranslationFormAccess will not grant access if this raises an + // exception, so we can call this without a try-catch block here. + $langcode = $this->mapper->getLangcode(); + + $this->sourceLanguage = $this->languageManager->getLanguage($langcode); // Get base language configuration to display in the form before setting the // language to use for the form. This avoids repetitively settings and diff --git a/core/modules/node/src/Tests/NodeTypeTranslationTest.php b/core/modules/node/src/Tests/NodeTypeTranslationTest.php index f86ccc4..1c3a768 100644 --- a/core/modules/node/src/Tests/NodeTypeTranslationTest.php +++ b/core/modules/node/src/Tests/NodeTypeTranslationTest.php @@ -23,7 +23,9 @@ class NodeTypeTranslationTest extends WebTestBase { * @var array */ public static $modules = array( + 'block', 'config_translation', + 'field_ui', 'node', ); @@ -53,6 +55,8 @@ protected function setUp() { $admin_permissions = array( 'administer content types', + 'administer node fields', + 'administer languages', 'administer site configuration', 'administer themes', 'translate configuration', @@ -144,6 +148,29 @@ public function testNodeTypeTitleLabelTranslation() { $this->assertText('Edited title'); $this->drupalGet("$langcode/node/add/$type"); $this->assertText('Translated title'); + + // Add an e-mail field. + $this->drupalPostForm("admin/structure/types/manage/$type/fields/add-field", array('new_storage_type' => 'email', 'label' => 'Email', 'field_name' => 'email'), 'Save and continue'); + $this->drupalPostForm(NULL, array(), 'Save field settings'); + $this->drupalPostForm(NULL, array(), 'Save settings'); + + $type = Unicode::strtolower($this->randomMachineName(16)); + $name = $this->randomString(); + $this->drupalCreateContentType(array('type' => $type, 'name' => $name)); + + // Set tabs. + $this->drupalPlaceBlock('local_tasks_block', array('primary' => TRUE)); + + // Change default language. + $this->drupalPostForm('admin/config/regional/language', array('site_default_language' => 'es'), 'Save configuration'); + + // Try re-using the email field. + $this->drupalGet("es/admin/structure/types/manage/$type/fields/add-field"); + $this->drupalPostForm(NULL, array('existing_storage_name' => 'field_email', 'existing_storage_label' => 'Email'), 'Save and continue'); + $this->assertResponse(200); + $this->drupalGet("es/admin/structure/types/manage/$type/fields/node.$type.field_email/translate"); + $this->assertResponse(200); + $this->assertText("The configuration objects have different language codes so they cannot be translated"); } }