diff --git a/core/includes/form.inc b/core/includes/form.inc index 42206a444369e5da83c51a94047c8dc2ef2d6665..91d2d6df08394e50d6f7f7d088167de01217572a 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -983,7 +983,7 @@ function form_select_options($element, $choices = NULL) { $options .= form_select_options($element, $choice); $options .= '</optgroup>'; } - elseif (is_object($choice)) { + elseif (is_object($choice) && isset($choice->option)) { $options .= form_select_options($element, $choice->option); } else { diff --git a/core/lib/Drupal/Core/Annotation/Translation.php b/core/lib/Drupal/Core/Annotation/Translation.php index ad32b5e196c0871b3fa70047666575c0ebe4396e..bb754b6a66ea864cb14cf6906d23d4f0676c7bab 100644 --- a/core/lib/Drupal/Core/Annotation/Translation.php +++ b/core/lib/Drupal/Core/Annotation/Translation.php @@ -8,7 +8,7 @@ namespace Drupal\Core\Annotation; use Drupal\Component\Annotation\AnnotationBase; -use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationWrapper; /** * @defgroup plugin_translatable Translatable plugin metadata @@ -52,12 +52,11 @@ * @ingroup plugin_translatable */ class Translation extends AnnotationBase { - use StringTranslationTrait; /** - * The translation of the value passed to the constructor of the class. + * The string translation object. * - * @var string + * @var \Drupal\Core\StringTranslation\TranslationWrapper */ protected $translation; @@ -83,11 +82,11 @@ public function __construct(array $values) { 'context' => $values['context'], ); } - $this->translation = $this->t($string, $arguments, $options); + $this->translation = new TranslationWrapper($string, $arguments, $options); } /** - * Implements Drupal\Core\Annotation\AnnotationInterface::get(). + * {@inheritdoc} */ public function get() { return $this->translation; diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php index 4a5fd1d7bcbc5a9d326b9b78a1e4282fd355663b..60a1d023e2995f143e5e56a6b37bbe32741b1566 100644 --- a/core/lib/Drupal/Core/Config/StorableConfigBase.php +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php @@ -183,7 +183,7 @@ protected function castValue($key, $value) { if ($element && ($element instanceof Undefined || $element instanceof Ignore)) { return $value; } - if ((is_scalar($value) || $value === NULL)) { + if (is_scalar($value) || $value === NULL) { if ($element && $element instanceof PrimitiveInterface) { // Special handling for integers and floats since the configuration // system is primarily concerned with saving values from the Form API diff --git a/core/lib/Drupal/Core/Form/OptGroup.php b/core/lib/Drupal/Core/Form/OptGroup.php index f94d62dc43f61442497e6338e1fbe17cbde5a52b..21e9855962d51c791de4d9f70a827d24ec8cfae4 100644 --- a/core/lib/Drupal/Core/Form/OptGroup.php +++ b/core/lib/Drupal/Core/Form/OptGroup.php @@ -43,7 +43,7 @@ public static function flattenOptions(array $array) { */ protected static function doFlattenOptions(array $array, array &$options) { foreach ($array as $key => $value) { - if (is_object($value)) { + if (is_object($value) && isset($value->option)) { static::doFlattenOptions($value->option, $options); } elseif (is_array($value)) { diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php new file mode 100644 index 0000000000000000000000000000000000000000..1285cb783f07e277d0cedc73e11069b70f22d908 --- /dev/null +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -0,0 +1,86 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\StringTranslation\TranslationWrapper. + */ + +namespace Drupal\Core\StringTranslation; + +/** + * Provides a class to wrap a translatable string. + * + * This class can be used to delay translating strings until the translation + * system is ready. This is useful for using translation in very low level + * subsystems like entity definition and stream wrappers. + * + * @see \Drupal\Core\Annotation\Translation + */ +class TranslationWrapper { + use StringTranslationTrait; + + /** + * The string to be translated. + * + * @var string + */ + protected $string; + + /** + * The translation arguments. + * + * @var array + */ + protected $arguments; + + /** + * The translation options. + * + * @var array + */ + protected $options; + + /** + * Constructs a new class instance. + * + * Parses values passed into this class through the t() function in Drupal and + * handles an optional context for the string. + * + * @param string $string + * The string that is to be translated. + * @param array $arguments + * (optional) An array with placeholder replacements, keyed by placeholder. + * @param array $options + * (optional) An array of additional options. + */ + public function __construct($string, array $arguments = array(), array $options = array()) { + $this->string = $string; + $this->arguments = $arguments; + $this->options = $options; + } + + /** + * Implements the magic __toString() method. + */ + public function __toString() { + return $this->render(); + } + + /** + * Renders the object as a string. + * + * @return string + * The translated string. + */ + public function render() { + return $this->t($this->string, $this->arguments, $this->options); + } + + /** + * Magic __sleep() method to avoid serializing the string translator. + */ + public function __sleep() { + return array('string', 'arguments', 'options'); + } + +} diff --git a/core/modules/block/src/BlockBase.php b/core/modules/block/src/BlockBase.php index 68f5f36189149058c106a611d8ff502eae3fa9c9..df3507c487a74be65001458165251277dfec904d 100644 --- a/core/modules/block/src/BlockBase.php +++ b/core/modules/block/src/BlockBase.php @@ -36,7 +36,9 @@ public function label() { } $definition = $this->getPluginDefinition(); - return $definition['admin_label']; + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + return (string) $definition['admin_label']; } /** diff --git a/core/modules/editor/src/Tests/EditorManagerTest.php b/core/modules/editor/src/Tests/EditorManagerTest.php index 997d861f7cbce9a3d899e39c7873832281a0d8a7..51cd2854b92f7c9276c5b623404abaa119188345 100644 --- a/core/modules/editor/src/Tests/EditorManagerTest.php +++ b/core/modules/editor/src/Tests/EditorManagerTest.php @@ -82,7 +82,7 @@ public function testManager() { $this->editorManager->clearCachedDefinitions(); // Case 2: a text editor available. - $this->assertIdentical(array('unicorn' => 'Unicorn Editor'), $this->editorManager->listOptions(), 'When some text editor is enabled, the manager works correctly.'); + $this->assertIdentical('Unicorn Editor', (string) $this->editorManager->listOptions()['unicorn'], 'When some text editor is enabled, the manager works correctly.'); // Case 3: a text editor available & associated (but associated only with // the 'Full HTML' text format). diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index f626203b8de0ce7cdeb7b5f46164f8a251fbc108..73c6db63d508af844d180caa861f92c815ea8738 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -15,6 +15,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\language\Entity\Language as LanguageEntity; use Drupal\Component\Utility\Crypt; use Symfony\Component\HttpFoundation\Request; @@ -209,9 +210,9 @@ function locale_theme() { function locale_stream_wrappers() { $wrappers = array( 'translations' => array( - 'name' => t('Translation files'), + 'name' => new TranslationWrapper('Translation files'), 'class' => 'Drupal\locale\TranslationsStream', - 'description' => t('Translation files'), + 'description' => new TranslationWrapper('Translation files'), 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN, ), ); diff --git a/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bf126c7caabec7a7cd1ead510cde1d414b8e4c98 --- /dev/null +++ b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php @@ -0,0 +1,53 @@ +<?php + +/** + * @file + * Contains \Drupal\locale\Tests\LocaleLocaleLookupTest. + */ + +namespace Drupal\locale\Tests; + +use Drupal\Core\Language\Language; +use Drupal\simpletest\WebTestBase; + +class LocaleLocaleLookupTest extends WebTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('locale'); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Test LocaleLookup', + 'description' => 'Tests LocaleLookup does not cause circular references.', + 'group' => 'Locale', + ); + } + + /** + * Tests hasTranslation(). + */ + public function testCircularDependency() { + // Change the language default object to different values. + $new_language_default = new Language(array( + 'id' => 'fr', + 'name' => 'French', + 'direction' => 0, + 'weight' => 0, + 'method_id' => 'language-default', + 'default' => TRUE, + )); + language_save($new_language_default); + $this->drupalLogin($this->root_user); + // Ensure that we can enable early_translation_test on a non-english site. + $this->drupalPostForm('admin/modules', array('modules[Testing][early_translation_test][enable]' => TRUE), t('Save configuration')); + $this->assertResponse(200); + } + +} diff --git a/core/modules/locale/tests/modules/early_translation_test/early_translation_test.info.yml b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..cfc6f7ea5be61f6a1d4bbb827405d61c96c03463 --- /dev/null +++ b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.info.yml @@ -0,0 +1,7 @@ +name: 'Early translation test' +type: module +description: 'Support module for testing early bootstrap getting of annotations with translations.' +core: 8.x +package: Testing +version: VERSION + diff --git a/core/modules/locale/tests/modules/early_translation_test/early_translation_test.services.yml b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..13718c287b8fb53eedca558a80f9323b57301eb7 --- /dev/null +++ b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.services.yml @@ -0,0 +1,6 @@ +services: + authentication.early_translation_test: + class: Drupal\early_translation_test\Auth + arguments: ['@entity.manager'] + tags: + - { name: authentication_provider, priority: 100 } diff --git a/core/modules/locale/tests/modules/early_translation_test/src/Auth.php b/core/modules/locale/tests/modules/early_translation_test/src/Auth.php new file mode 100644 index 0000000000000000000000000000000000000000..afb5ea1685f554b2484a1f0d3244604e0f97b612 --- /dev/null +++ b/core/modules/locale/tests/modules/early_translation_test/src/Auth.php @@ -0,0 +1,68 @@ +<?php + +/** + * @file + * Contains \Drupal\early_translation_test\Auth. + */ + +namespace Drupal\early_translation_test; + +use Drupal\Core\Authentication\AuthenticationProviderInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + +/** + * Test authentication provider. + */ +class Auth implements AuthenticationProviderInterface { + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * Constructs an authentication provider object. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager service. + */ + public function __construct(EntityManagerInterface $entity_manager) { + // Authentication providers are called early during in the bootstrap. + // Getting the user storage used to result in a circular reference since + // translation involves a call to \Drupal\locale\LocaleLookup that tries to + // get the user roles. + // @see https://drupal.org/node/2241461 + $this->userStorage = $entity_manager->getStorage('user'); + } + + /** + * {@inheritdoc} + */ + public function applies(Request $request) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function authenticate(Request $request) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function cleanup(Request $request) {} + + /** + * {@inheritdoc} + */ + public function handleException(GetResponseForExceptionEvent $event) { + return FALSE; + } + +} diff --git a/core/modules/system/src/Tests/Plugin/Discovery/DiscoveryTestBase.php b/core/modules/system/src/Tests/Plugin/Discovery/DiscoveryTestBase.php index 9d0be5469665fe32637da613805b97b3bd1328c9..d58cf03770bc274658df2cc2357c756203e2bfd1 100644 --- a/core/modules/system/src/Tests/Plugin/Discovery/DiscoveryTestBase.php +++ b/core/modules/system/src/Tests/Plugin/Discovery/DiscoveryTestBase.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Plugin\Discovery; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\simpletest\UnitTestBase; /** @@ -49,7 +50,7 @@ function testDiscoveryInterface() { // Ensure that getDefinition() returns the expected definition. foreach ($this->expectedDefinitions as $id => $definition) { - $this->assertIdentical($this->discovery->getDefinition($id), $definition); + $this->assertDefinitionIdentical($this->discovery->getDefinition($id), $definition); } // Ensure that an empty array is returned if no plugin definitions are found. @@ -58,5 +59,30 @@ function testDiscoveryInterface() { // Ensure that NULL is returned as the definition of a non-existing plugin. $this->assertIdentical($this->emptyDiscovery->getDefinition('non_existing', FALSE), NULL, 'NULL returned as the definition of a non-existing plugin.'); } + + /** + * Asserts a definition against an expected definition. + * + * Converts any instances of \Drupal\Core\Annotation\Translation to a string. + * + * @param array $definition + * The definition to test. + * @param array $expected_definition + * The expected definition to test against. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertDefinitionIdentical(array $definition, array $expected_definition) { + $func = function (&$item){ + if ($item instanceof TranslationWrapper) { + $item = (string) $item; + } + }; + array_walk_recursive($definition, $func); + array_walk_recursive($expected_definition, $func); + return $this->assertIdentical($definition, $expected_definition); + } + } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ad3a014b53674ce63fbd29a3a2ceb3652a18a44a..6aea5b886cb855a3c326e48a77ede5d15c369560 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -9,6 +9,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\block\BlockPluginInterface; use Drupal\user\UserInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -719,15 +720,15 @@ function system_theme_suggestions_field(array $variables) { function system_stream_wrappers() { $wrappers = array( 'public' => array( - 'name' => t('Public files'), + 'name' => new TranslationWrapper('Public files'), 'class' => 'Drupal\Core\StreamWrapper\PublicStream', - 'description' => t('Public local files served by the webserver.'), + 'description' => new TranslationWrapper('Public local files served by the webserver.'), 'type' => STREAM_WRAPPERS_LOCAL_NORMAL, ), 'temporary' => array( - 'name' => t('Temporary files'), + 'name' => new TranslationWrapper('Temporary files'), 'class' => 'Drupal\Core\StreamWrapper\TemporaryStream', - 'description' => t('Temporary local files for upload and previews.'), + 'description' => new TranslationWrapper('Temporary local files for upload and previews.'), 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN, ), ); @@ -735,9 +736,9 @@ function system_stream_wrappers() { // Only register the private file stream wrapper if a file path has been set. if (\Drupal::config('system.file')->get('path.private')) { $wrappers['private'] = array( - 'name' => t('Private files'), + 'name' => new TranslationWrapper('Private files'), 'class' => 'Drupal\Core\StreamWrapper\PrivateStream', - 'description' => t('Private local files served by Drupal.'), + 'description' => new TranslationWrapper('Private local files served by Drupal.'), 'type' => STREAM_WRAPPERS_LOCAL_NORMAL, ); } diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index 87a56745062f90a22e4378d7d1f31387f0c4ec48..db5ba76104541eb2bcbd537bf10db80fe75c8154 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -195,7 +195,9 @@ public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) { $display_options = array( 'display_plugin' => $plugin_id, 'id' => $id, - 'display_title' => $title, + // Cast the display title to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + 'display_title' => (string) $title, 'position' => $id === 'default' ? 0 : count($this->display), 'provider' => $plugin['provider'], 'display_options' => array(), diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 902b5438a226f3e948665d26e9beb147a5db511d..562ed680e5b14a7a8276cfb214bb403467706551 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -1160,11 +1160,13 @@ protected function prepareFilterSelectOptions(&$options) { } // FAPI has some special value to allow hierarchy. // @see _form_options_flatten - elseif (is_object($label)) { + elseif (is_object($label) && isset($label->option)) { $this->prepareFilterSelectOptions($options[$value]->option); } else { - $options[$value] = strip_tags(decode_entities($label)); + // Cast the label to a string since it can be an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + $options[$value] = strip_tags(decode_entities((string) $label)); } } } diff --git a/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php b/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php index f1b7b586a8fbf9527ab4575d7f278583b8f03db0..ec40410a8a54da3af8650d6b3bd6b920abc290b1 100644 --- a/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php +++ b/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php @@ -79,7 +79,9 @@ protected function defineOptions() { // Relationships definitions should define a default label, but if they aren't get another default value. if (!empty($this->definition['label'])) { - $label = $this->definition['label']; + // Cast the label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + $label = (string) $this->definition['label']; } else { $label = !empty($this->definition['field']) ? $this->definition['field'] : $this->definition['base field']; diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index 68ad5b07081a2ba61bd2efbc678da8d58de9dfd4..a8d3b81118d1a2dfbf79cf854ce4ed5a363ddcd1 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -229,13 +229,15 @@ protected function getDisplaysList(EntityInterface $view) { $displays = array(); foreach ($view->get('display') as $display) { $definition = $this->displayManager->getDefinition($display['display_plugin']); - if (!empty($definition['admin'])) { - $displays[$definition['admin']] = TRUE; + if (isset($definition['admin'])) { + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + $displays[] = (string) $definition['admin']; } } - ksort($displays); - return array_keys($displays); + sort($displays); + return $displays; } /** diff --git a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php index 071df3e8039c2d405283513154c84a559dcec167..bcfe00df1e1b1655cf9fa3c178caaa40ffab1358 100644 --- a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php +++ b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php @@ -62,7 +62,7 @@ public function testGet(array $values, $expected) { $annotation = new Translation($values); - $this->assertSame($expected, $annotation->get()); + $this->assertSame($expected, (string) $annotation->get()); } /** diff --git a/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php b/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php index 0807448e93de45f950ccae202c055bd8d15d069a..febc6c68ba49c656bf9349b615a20149b2a4f0f7 100644 --- a/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php +++ b/core/tests/Drupal/Tests/Core/Form/OptGroupTest.php @@ -50,11 +50,15 @@ public function providerTestFlattenOptions() { $object1->option = array('foo' => 'foo'); $object2 = new \stdClass(); $object2->option = array(array('foo' => 'foo'), array('foo' => 'foo')); + $object3 = new \stdClass(); return array( array(array('foo' => 'foo')), array(array(array('foo' => 'foo'))), array(array($object1)), array(array($object2)), + array(array($object1, $object2)), + array(array('foo' => $object3)), + array(array('foo' => $object3, $object1, array('foo' => 'foo'))), ); }