diff --git a/core/includes/form.inc b/core/includes/form.inc index 42206a4..91d2d6d 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 .= ''; } - 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 ad32b5e..bb754b6 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 4a5fd1d..60a1d02 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 f94d62d..21e9855 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 0000000..1285cb7 --- /dev/null +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -0,0 +1,86 @@ +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 68f5f36..df3507c 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 997d861..51cd285 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 d3e05dc..a9e6dd1 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; @@ -210,9 +211,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 0000000..1d1bd16 --- /dev/null +++ b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php @@ -0,0 +1,50 @@ + '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 0000000..cfc6f7e --- /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 0000000..13718c2 --- /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 0000000..521cbc8 --- /dev/null +++ b/core/modules/locale/tests/modules/early_translation_test/src/Auth.php @@ -0,0 +1,63 @@ +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 9d0be54..415e0b9 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 + * 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 6bd8dd4..1390127 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 87a5674..db5ba76 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 902b543..562ed68 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 f1b7b58..ec40410 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 68ad5b0..21c3172 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -229,8 +229,10 @@ 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; + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + if (isset($definition['admin']) && $display_admin = (string) $definition['admin']) { + $displays[$display_admin] = TRUE; } } diff --git a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php index 071df3e..bcfe00d 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 0807448..febc6c6 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'))), ); }