diff -u b/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php --- b/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php +++ b/core/modules/ckeditor/src/Tests/CKEditorAdminTest.php @@ -87,23 +87,23 @@ // Button groups array( array( - 'name' => t('Formatting'), + 'name' => 'Formatting', 'items' => array('Bold', 'Italic',), ), array( - 'name' => t('Links'), + 'name' => 'Links', 'items' => array('DrupalLink', 'DrupalUnlink',), ), array( - 'name' => t('Lists'), + 'name' => 'Lists', 'items' => array('BulletedList', 'NumberedList',), ), array( - 'name' => t('Media'), + 'name' => 'Media', 'items' => array('Blockquote', 'DrupalImage',), ), array( - 'name' => t('Tools'), + 'name' => 'Tools', 'items' => array('Source',), ), ), only in patch2: unchanged: --- a/core/lib/Drupal/Core/Cache/CacheCollector.php +++ b/core/lib/Drupal/Core/Cache/CacheCollector.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Cache; +use Drupal\Component\Utility\SafeStringInterface; use Drupal\Core\DestructableInterface; use Drupal\Core\Lock\LockBackendInterface; @@ -145,6 +146,11 @@ public function has($key) { */ public function get($key) { $this->lazyLoadCache(); + // As a development aid, we allow calling code to use keys that are objects + // implementing SafeStringInterface. + if ($key instanceof SafeStringInterface) { + $key = (string) $key; + } if (isset($this->storage[$key]) || array_key_exists($key, $this->storage)) { return $this->storage[$key]; } only in patch2: unchanged: --- a/core/lib/Drupal/Core/Cache/CacheCollectorInterface.php +++ b/core/lib/Drupal/Core/Cache/CacheCollectorInterface.php @@ -26,7 +26,7 @@ /** * Gets value from the cache. * - * @param string $key + * @param string|\Drupal\Component\Utility\SafeStringInterface $key * Key that identifies the data. * * @return mixed only in patch2: unchanged: --- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php @@ -41,6 +41,24 @@ public function translate($string, array $args = array(), array $options = array()); /** + * Translates a string to the current language or to a given language. + * + * @param string $string + * A string containing the English string to translate. + * @param array $options + * An associative array of additional options, with the following elements: + * - 'langcode': The language code to translate to a language other than + * what is used to display the page. + * - 'context': The context the source string belongs to. + * + * @return string + * The translated string. + * + * @internal + */ + public function doTranslate($string, array $options = array()); + + /** * Formats a string containing a count of items. * * This function ensures that the string is pluralized correctly. Since t() is only in patch2: unchanged: --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -140,38 +140,13 @@ public function getStringTranslation($langcode, $string, $context) { * {@inheritdoc} */ public function translate($string, array $args = array(), array $options = array()) { - $string = $this->doTranslate($string, $options); - if (empty($args)) { - // We add the string to the safe list as opposed to making it an object - // implementing SafeStringInterface as we may need to call __toString() - // on the object before render time, at which point the string ceases to - // be safe, and working around this would require significant rework. - // Adding this string to the safe list is assumed to be safe because - // translate() should only be called with strings defined in code. - // @see \Drupal\Core\StringTranslation\TranslationInterface::translate() - SafeMarkup::setMultiple([$string => ['html' => TRUE]]); - return $string; - } - else { - return SafeMarkup::format($string, $args); - } + return new TranslationWrapper($string, $args, $options); } /** - * Translates a string to the current language or to a given language. - * - * @param string $string - * A string containing the English string to translate. - * @param array $options - * An associative array of additional options, with the following elements: - * - 'langcode': The language code to translate to a language other than - * what is used to display the page. - * - 'context': The context the source string belongs to. - * - * @return string - * The translated string. + * {@inheritdoc} */ - protected function doTranslate($string, array $options = array()) { + public function doTranslate($string, array $options = array()) { // Merge in defaults. if (empty($options['langcode'])) { $options['langcode'] = $this->defaultLangcode; only in patch2: unchanged: --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -7,6 +7,8 @@ namespace Drupal\Core\StringTranslation; +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeStringInterface; use Drupal\Component\Utility\ToStringTrait; @@ -103,7 +105,36 @@ public function getOptions() { * The translated string. */ public function render() { - return $this->t($this->string, $this->arguments, $this->options); + $string = $this->getStringTranslation()->doTranslate($this->string, $this->options); + // @todo fix this pending a decision on https://www.drupal.org/node/2506427 + if (!empty($this->arguments)) { + // Transform arguments before inserting them. + $args = $this->arguments; + foreach ($args as $key => $value) { + switch ($key[0]) { + case '@': + if (!SafeMarkup::isSafe($value)) { + // Escaped only. + $args[$key] = Html::escape($value); + } + break; + + case '%': + default: + // Escaped and placeholder. + if (!SafeMarkup::isSafe($value)) { + $value = Html::escape($value); + } + $args[$key] = '' . $value . ''; + break; + + case '!': + // Pass-through. This should be safe. + } + } + $string = strtr($string, $args); + } + return $string; } /** @@ -123,5 +154,4 @@ public function jsonSerialize() { return $this->__toString(); } - } only in patch2: unchanged: --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -109,7 +109,8 @@ protected function createAttributeValue($name, $value) { elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); } - elseif (!is_object($value)) { + // As a development aid, we allow the value to be a safe string object. + elseif (!is_object($value) || $value instanceof SafeStringInterface) { $value = new AttributeString($name, $value); } return $value; only in patch2: unchanged: --- a/core/lib/Drupal/Core/Validation/DrupalTranslator.php +++ b/core/lib/Drupal/Core/Validation/DrupalTranslator.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Validation; +use Drupal\Component\Utility\SafeStringInterface; + /** * Translates strings using Drupal's translation system. * @@ -73,8 +75,13 @@ public function getLocale() { protected function processParameters(array $parameters) { $return = array(); foreach ($parameters as $key => $value) { + // We allow the values in the parameters to be safe string objects. This can be + // useful when we want to use parameter values that are TranslationWrappers. + if ($value instanceof SafeStringInterface) { + $value = (string) $value; + } if (is_object($value)) { - // t() does not work will objects being passed as replacement strings. + // t() does not work with objects being passed as replacement strings. } // Check for symfony replacement patterns in the form "{{ name }}". elseif (strpos($key, '{{ ') === 0 && strrpos($key, ' }}') == strlen($key) - 3) { only in patch2: unchanged: --- a/core/modules/comment/src/CommentForm.php +++ b/core/modules/comment/src/CommentForm.php @@ -294,7 +294,9 @@ public function buildEntity(array $form, FormStateInterface $form_state) { // Edge cases where the comment body is populated only by HTML tags will // require a default subject. if ($comment->getSubject() == '') { - $comment->setSubject($this->t('(No subject)')); + // @todo fix this. We cast to string so this will pass the + // primitive type constraint validation. + $comment->setSubject((string) $this->t('(No subject)')); } } return $comment; only in patch2: unchanged: --- a/core/modules/locale/src/Tests/LocaleImportFunctionalTest.php +++ b/core/modules/locale/src/Tests/LocaleImportFunctionalTest.php @@ -238,8 +238,10 @@ public function testLanguageContext() { 'langcode' => 'hr', )); - $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', 'Long month name context is working.'); - $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', 'Default context is working.'); + // We cast the return value of t() to string so as to retrieve the translated + // value, rendered as a string. + $this->assertIdentical((string) t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', 'Long month name context is working.'); + $this->assertIdentical((string) t('May', array(), array('langcode' => 'hr')), 'Svi.', 'Default context is working.'); } /** @@ -254,7 +256,7 @@ public function testEmptyMsgstr() { )); $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); - $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', 'String imported and translated.'); + $this->assertIdentical((string) t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', 'String imported and translated.'); // Try importing a .po file. $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array( only in patch2: unchanged: --- a/core/modules/locale/src/Tests/LocaleTranslationUiTest.php +++ b/core/modules/locale/src/Tests/LocaleTranslationUiTest.php @@ -64,7 +64,7 @@ public function testStringTranslation() { ); $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + (string) t($name, array(), array('langcode' => $langcode)); // Reset locale cache. $this->container->get('string_translation')->reset(); $this->assertRaw('"edit-languages-' . $langcode . '-weight"', 'Language code found.'); @@ -302,7 +302,7 @@ public function testStringValidation() { ); $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + (string) t($name, array(), array('langcode' => $langcode)); // Reset locale cache. $search = array( 'string' => $name, @@ -361,7 +361,7 @@ public function testStringSearch() { $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); // Add string. - t($name, array(), array('langcode' => $langcode)); + (string) t($name, array(), array('langcode' => $langcode)); // Reset locale cache. $this->container->get('string_translation')->reset(); $this->drupalLogout(); only in patch2: unchanged: --- a/core/modules/search/src/Plugin/SearchPluginBase.php +++ b/core/modules/search/src/Plugin/SearchPluginBase.php @@ -124,7 +124,9 @@ public function suggestedTitle() { // If the user entered a search string, truncate it and append it to the // title. if (!empty($this->keywords)) { - return $this->t('Search for @keywords', array('@keywords' => Unicode::truncate($this->keywords, 60, TRUE, TRUE))); + // !!!! This will already be auto-escaped on output, so as a temporary workaround we use a !placeholder to prevent double escaping. + // @todo fix this elsewhere, if we use @placeholder the head_title will be double-escaped in template_preprocess_html(). + return $this->t('Search for !keywords', array('!keywords' => Unicode::truncate($this->keywords, 60, TRUE, TRUE))); } // Use the default 'Search' title. return $this->t('Search'); only in patch2: unchanged: --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -311,7 +311,8 @@ function shortcut_preprocess_page(&$variables) { $query = array( 'link' => $link, - 'name' => $variables['title'], + // @todo document why we cast the title to string. + 'name' => (string) $variables['title'], ); $shortcut_set = shortcut_current_displayed_set(); only in patch2: unchanged: --- a/core/modules/system/src/Tests/Form/FormTest.php +++ b/core/modules/system/src/Tests/Form/FormTest.php @@ -144,7 +144,7 @@ function testRequiredFields() { // Select elements are going to have validation errors with empty // input, since those are illegal choices. Just make sure the // error is not "field is required". - $this->assertTrue((empty($errors[$element]) || strpos('field is required', $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element"); + $this->assertTrue((empty($errors[$element]) || strpos('field is required', (string) $errors[$element]) === FALSE), "Optional '$type' field '$element' is not treated as a required element"); } else { // Make sure there is *no* form error for this element. only in patch2: unchanged: --- a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php +++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php @@ -40,7 +40,7 @@ public function __construct() { // A simple plugin: the user login block. $this->discovery->setDefinition('user_login', array( - 'label' => t('User login'), + 'label' => 'User login', 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock', )); @@ -67,7 +67,7 @@ public function __construct() { // MockLayoutBlockDeriver class ensures that both the base plugin and the // derivatives are available to the system. $this->discovery->setDefinition('layout', array( - 'label' => t('Layout'), + 'label' => 'Layout', 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock', 'deriver' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlockDeriver', )); @@ -78,7 +78,7 @@ public function __construct() { 'label' => t('User name'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User')), + 'user' => new ContextDefinition('entity:user', 'User'), ), )); @@ -87,7 +87,7 @@ public function __construct() { 'label' => t('User name optional'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User'), FALSE), + 'user' => new ContextDefinition('entity:user', 'User', FALSE), ), )); @@ -102,8 +102,8 @@ public function __construct() { 'label' => t('Complex context'), 'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockComplexContextBlock', 'context' => array( - 'user' => new ContextDefinition('entity:user', t('User')), - 'node' => new ContextDefinition('entity:node', t('Node')), + 'user' => new ContextDefinition('entity:user', 'User'), + 'node' => new ContextDefinition('entity:node', 'Node'), ), )); only in patch2: unchanged: --- a/core/modules/user/src/Tests/UserCancelTest.php +++ b/core/modules/user/src/Tests/UserCancelTest.php @@ -535,7 +535,7 @@ function testMassUserCancelByAdmin() { $this->drupalPostForm(NULL, NULL, t('Cancel accounts')); $status = TRUE; foreach ($users as $account) { - $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->getUsername()))) !== FALSE); + $status = $status && (strpos($this->content, $account->getUsername() . ' has been deleted.') !== FALSE); $user_storage->resetCache(array($account->id())); $status = $status && !$user_storage->load($account->id()); } only in patch2: unchanged: --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -991,7 +991,8 @@ function user_user_role_insert(RoleInterface $role) { $action = entity_create('action', array( 'id' => $add_id, 'type' => 'user', - 'label' => t('Add the @label role to the selected users', array('@label' => $role->label())), + // @todo investigate + 'label' => (string) t('Add the @label role to the selected users', array('@label' => $role->label())), 'configuration' => array( 'rid' => $role->id(), ), @@ -1004,7 +1005,8 @@ function user_user_role_insert(RoleInterface $role) { $action = entity_create('action', array( 'id' => $remove_id, 'type' => 'user', - 'label' => t('Remove the @label role from the selected users', array('@label' => $role->label())), + // @todo investigate + 'label' => (string) t('Remove the @label role from the selected users', array('@label' => $role->label())), 'configuration' => array( 'rid' => $role->id(), ), only in patch2: unchanged: --- a/core/modules/views/src/Plugin/Derivative/ViewsBlock.php +++ b/core/modules/views/src/Plugin/Derivative/ViewsBlock.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Component\Utility\Xss; /** * Provides block plugin definitions for all Views block displays. @@ -93,7 +94,9 @@ public function getDerivativeDefinitions($base_plugin_definition) { if (empty($desc)) { if ($display->display['display_title'] == $display->definition['title']) { - $desc = t('!view', array('!view' => $view->label())); + // @todo fix this hack, it should not be needed -- we should sanitize + // during rendering instead. + $desc = t('!view', array('!view' => Xss::filterAdmin($view->label()))); } else { $desc = t('!view: !display', array('!view' => $view->label(), '!display' => $display->display['display_title'])); only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php +++ b/core/tests/Drupal/Tests/Core/Annotation/TranslationTest.php @@ -46,8 +46,8 @@ public function testGet(array $values, $expected) { 'context' => $values['context'], ) : array(); $this->translationManager->expects($this->once()) - ->method('translate') - ->with($values['value'], $arguments, $options); + ->method('doTranslate') + ->with($values['value'], $options); $annotation = new Translation($values); @@ -61,9 +61,9 @@ public function providerTestGet() { $data = array(); $data[] = array( array( - 'value' => 'Foo', + 'value' => 'Foo' ), - 'Foo' + 'Foo', ); $random = $this->randomMachineName(); $random_html_entity = '&' . $random; only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php @@ -74,8 +74,8 @@ public function testGetTitle() { $this->pluginDefinition['title'] = (new TranslationWrapper($title)) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array()) + ->method('doTranslate') + ->with($title, array()) ->will($this->returnValue('Example translated')); $this->setupContextualLinkDefault(); @@ -90,8 +90,8 @@ public function testGetTitleWithContext() { $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array('context' => 'context')) + ->method('doTranslate') + ->with($title, array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupContextualLinkDefault(); @@ -106,8 +106,8 @@ public function testGetTitleWithTitleArguments() { $this->pluginDefinition['title'] = (new TranslationWrapper($title, array('@test' => 'value'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array('@test' => 'value'), array()) + ->method('doTranslate') + ->with($title, array()) ->will($this->returnValue('Example value')); $this->setupContextualLinkDefault(); only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionDefaultTest.php @@ -86,8 +86,8 @@ public function testGetTitle() { $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array()) + ->method('doTranslate') + ->with('Example', array()) ->will($this->returnValue('Example translated')); $this->setupLocalActionDefault(); @@ -103,8 +103,8 @@ public function testGetTitleWithContext() { $this->pluginDefinition['title'] = (new TranslationWrapper('Example', array(), array('context' => 'context'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array('context' => 'context')) + ->method('doTranslate') + ->with('Example', array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupLocalActionDefault(); @@ -118,8 +118,8 @@ public function testGetTitleWithTitleArguments() { $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example @test', array('@test' => 'value'), array()) + ->method('doTranslate') + ->with('Example @test', array()) ->will($this->returnValue('Example value')); $this->setupLocalActionDefault(); only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskDefaultTest.php @@ -236,8 +236,8 @@ public function testGetTitle() { $this->pluginDefinition['title'] = (new TranslationWrapper('Example')) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with('Example', array(), array()) + ->method('doTranslate') + ->with('Example', array()) ->will($this->returnValue('Example translated')); $this->setupLocalTaskDefault(); @@ -252,8 +252,8 @@ public function testGetTitleWithContext() { $this->pluginDefinition['title'] = (new TranslationWrapper($title, array(), array('context' => 'context'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array(), array('context' => 'context')) + ->method('doTranslate') + ->with($title, array('context' => 'context')) ->will($this->returnValue('Example translated with context')); $this->setupLocalTaskDefault(); @@ -268,8 +268,8 @@ public function testGetTitleWithTitleArguments() { $this->pluginDefinition['title'] = (new TranslationWrapper('Example @test', array('@test' => 'value'))) ->setStringTranslation($this->stringTranslation); $this->stringTranslation->expects($this->once()) - ->method('translate') - ->with($title, array('@test' => 'value'), array()) + ->method('doTranslate') + ->with($title, array()) ->will($this->returnValue('Example value')); $this->setupLocalTaskDefault(); only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php +++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php @@ -66,7 +66,7 @@ public function testToString() { ->willReturn(''); $translation = $this->prophesize(TranslationInterface::class); - $translation->translate($string, [], [])->will(function () { + $translation->doTranslate($string, [])->will(function () { throw new \Exception('Yes you may.'); }); $text->setStringTranslation($translation->reveal()); only in patch2: unchanged: --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -220,6 +220,11 @@ public function getStringTranslationStub() { ->willReturnCallback(function ($count, $singular, $plural, array $args = [], array $options = []) { return $count === 1 ? SafeMarkup::format($singular, $args) : SafeMarkup::format($plural, $args + ['@count' => $count]); }); + $translation->expects($this->any()) + ->method('doTranslate') + ->willReturnCallback(function ($string, array $options = []) { + return $string; + }); return $translation; }