diff --git a/core/core.services.yml b/core/core.services.yml index 98ddc9ec13f2608e6d35474d6b2e06104a8e50a9..05be1bb0e4f0ee5eacab40bb81c4392bf38d6c4f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -508,7 +508,7 @@ services: arguments: ['@request_stack', '@config.factory' ] link_generator: class: Drupal\Core\Utility\LinkGenerator - arguments: ['@url_generator', '@module_handler'] + arguments: ['@url_generator', '@module_handler', '@renderer'] router: class: Drupal\Core\Routing\AccessAwareRouter arguments: ['@router.no_access_checks', '@access_manager', '@current_user'] diff --git a/core/includes/common.inc b/core/includes/common.inc index d05ee0c537f05da16dd45fa6dbc1d6bee8079f15..b28538c6bb15320967106fd97d942c9d97ffdeb2 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -638,6 +638,8 @@ function drupal_http_header_attributes(array $attributes = array()) { * * @param string|array $text * The link text for the anchor tag as a translated string or render array. + * Strings will be sanitized automatically. If you need to output HTML in the + * link text you should use a render array. * @param string $path * The internal path or external URL being linked to, such as "node/34" or * "http://example.com/foo". After the url() function is called to construct @@ -653,11 +655,6 @@ function drupal_http_header_attributes(array $attributes = array()) { * must be a string; other elements are more flexible, as they just need * to work as an argument for the constructor of the class * Drupal\Core\Template\Attribute($options['attributes']). - * - 'html' (default FALSE): Whether $text is HTML or just plain-text. For - * example, to make an image tag into a link, this must be set to TRUE, or - * you will see the escaped HTML image tag. $text is not sanitized if - * 'html' is TRUE. The calling function must ensure that $text is already - * safe. * - 'language': An optional language object. If the path being linked to is * internal to the site, $options['language'] is used to determine whether * the link is "active", or pointing to the current page (the language as @@ -710,7 +707,6 @@ function _l($text, $path, array $options = array()) { $variables['options'] += array( 'attributes' => array(), 'query' => array(), - 'html' => FALSE, 'language' => NULL, 'set_active_class' => FALSE, ); @@ -755,8 +751,9 @@ function _l($text, $path, array $options = array()) { // in an HTML argument context, we need to encode it properly. $url = String::checkPlain(_url($variables['path'], $variables['options'])); - // Sanitize the link text if necessary. - $text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); + // Sanitize the link text. + $text = SafeMarkup::escape($variables['text']); + return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $text . '</a>'); } diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 4cd5d73cbaae8de6bab46f46f6420d0db8195f2a..0028de491f8d5fe3540522825662cd779a35e54f 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -327,26 +327,30 @@ function template_preprocess_menu_local_task(&$variables) { $link += array( 'localized_options' => array(), ); - $link_text = $link['title']; if (!empty($variables['element']['#active'])) { // Add text to indicate active tab for non-visual users. - $active = '<span class="visually-hidden">' . t('(active tab)') . '</span>'; $variables['attributes']['class'] = array('active'); - // If the link does not contain HTML already, String::checkPlain() it now. - // After we set 'html'=TRUE the link will not be sanitized by l(). - if (empty($link['localized_options']['html'])) { - $link['title'] = String::checkPlain($link['title']); - } - $link['localized_options']['html'] = TRUE; - $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active)); + // Build up an inline template which will be autoescaped. + $link_text = array( + '#type' => 'inline_template', + '#template' => '{{ title }}<span class="visually-hidden">{% trans %}(active tab){% endtrans %}></span>', + '#context' => array('title' => $link['title']), + ); + $title = drupal_render($link_text); } + else { + // @todo Remove expicit escaping when https://www.drupal.org/node/2338081 + // gets fixed. + $title = String::checkPlain($link['title']); + } + $link['localized_options']['set_active_class'] = TRUE; $variables['link'] = array( '#type' => 'link', - '#title' => $link_text, + '#title' => $title, '#url' => $link['url'], '#options' => $link['localized_options'], ); diff --git a/core/includes/tablesort.inc b/core/includes/tablesort.inc index 0258a7674223477ea88f1e5570f7178a9f544235..db8eff66591634e830489ed31a3f162e8e8e905c 100644 --- a/core/includes/tablesort.inc +++ b/core/includes/tablesort.inc @@ -43,6 +43,11 @@ function tablesort_init($header) { function tablesort_header(&$cell_content, array &$cell_attributes, array $header, array $ts) { // Special formatting for the currently sorted column header. if (isset($cell_attributes['field'])) { + $text = array( + 'cell_content' => array( + '#markup' => $cell_content, + ), + ); $title = t('sort by @s', array('@s' => $cell_content)); if ($cell_content == $ts['name']) { // aria-sort is a WAI-ARIA property that indicates if items in a table @@ -51,24 +56,24 @@ function tablesort_header(&$cell_content, array &$cell_attributes, array $header $cell_attributes['aria-sort'] = ($ts['sort'] == 'asc') ? 'ascending' : 'descending'; $ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc'); $cell_attributes['class'][] = 'active'; - $tablesort_indicator = array( - '#theme' => 'tablesort_indicator', - '#style' => $ts['sort'], - ); - $image = drupal_render($tablesort_indicator); } else { - // If the user clicks a different header, we want to sort ascending initially. + // If the user clicks a different header, we want to sort ascending + // initially. $ts['sort'] = 'asc'; - $image = ''; } - $cell_content = \Drupal::l($cell_content . $image, new Url('<current>', [], [ + + // Append the sort indicator to the cell content. + $text['image'] = [ + '#theme' => 'tablesort_indicator', + '#style' => $ts['sort'], + ]; + $cell_content = \Drupal::l($text, new Url('<current>', [], [ 'attributes' => array('title' => $title), 'query' => array_merge($ts['query'], array( 'sort' => $ts['sort'], 'order' => $cell_content, )), - 'html' => TRUE, ])); unset($cell_attributes['field'], $cell_attributes['sort']); diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 2c8f3fd02e7a4e549f7aa1d3cd6d7f6d5983a6a1..a73a701e77e7af31ad03e35d61a8bcb73ec6cfbe 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -580,9 +580,6 @@ function template_preprocess_status_messages(&$variables) { * - title: The link text. * - url: (optional) The url object to link to. If omitted, no a tag is * printed out. - * - html: (optional) Whether or not 'title' is HTML. If set, the title - * will not be passed through - * \Drupal\Component\Utility\String::checkPlain(). * - attributes: (optional) Attributes for the anchor, or for the <span> * tag used in its place if no 'href' is supplied. If element 'class' is * included, it must be an array of one or more class names. @@ -664,7 +661,7 @@ function template_preprocess_links(&$variables) { $keys = ['title', 'url']; $link_element = array( '#type' => 'link', - '#title' => $link['title'], + '#title' => is_array($link['title']) ? drupal_render($link['title']) : SafeMarkup::escape($link['title']), '#options' => array_diff_key($link, array_combine($keys, $keys)), '#url' => $link['url'], '#ajax' => $link['ajax'], @@ -706,8 +703,7 @@ function template_preprocess_links(&$variables) { } // Handle title-only text items. - $text = (!empty($link['html']) ? $link['title'] : String::checkPlain($link['title'])); - $item['text'] = $text; + $item['text'] = $link_element['#title']; if (isset($link['attributes'])) { $item['text_attributes'] = new Attribute($link['attributes']); } diff --git a/core/lib/Drupal/Core/Render/Element/Actions.php b/core/lib/Drupal/Core/Render/Element/Actions.php index eed3d11b3d57aa230502b103e1b834a7efa67219..754f7f8ac292671e8a44629517a3b053a30a22a7 100644 --- a/core/lib/Drupal/Core/Render/Element/Actions.php +++ b/core/lib/Drupal/Core/Render/Element/Actions.php @@ -97,7 +97,6 @@ public static function preRenderActionsDropbutton(&$element, FormStateInterface $button = drupal_render($element[$key]); $dropbuttons[$dropbutton]['#links'][$key] = array( 'title' => $button, - 'html' => TRUE, ); } } diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php index a6e8326248e71b827a489ce23e84a4e2357175c9..997a6baab5480d6e3c32c66ee6124c1101038de3 100644 --- a/core/lib/Drupal/Core/Utility/LinkGenerator.php +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -12,7 +12,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Link; -use Drupal\Core\Path\AliasManagerInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; @@ -36,6 +36,13 @@ class LinkGenerator implements LinkGeneratorInterface { */ protected $moduleHandler; + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Constructs a LinkGenerator instance. * @@ -43,10 +50,13 @@ class LinkGenerator implements LinkGeneratorInterface { * The url generator. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. */ - public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler) { + public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, RendererInterface $renderer) { $this->urlGenerator = $url_generator; $this->moduleHandler = $module_handler; + $this->renderer = $renderer; } /** @@ -75,8 +85,7 @@ public function generate($text, Url $url) { // Start building a structured representation of our link to be altered later. $variables = array( - // @todo Inject the service when drupal_render() is converted to one. - 'text' => is_array($text) ? drupal_render($text) : $text, + 'text' => is_array($text) ? $this->renderer->render($text) : $text, 'url' => $url, 'options' => $url->getOptions(), ); @@ -85,7 +94,6 @@ public function generate($text, Url $url) { $variables['options'] += array( 'attributes' => array(), 'query' => array(), - 'html' => FALSE, 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE, @@ -135,9 +143,10 @@ public function generate($text, Url $url) { // it here in an HTML argument context, we need to encode it properly. $url = String::checkPlain($url->toString()); - // Sanitize the link text if necessary. - $text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); - return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $text . '</a>'); + // Make sure the link text is sanitized. + $safe_text = SafeMarkup::escape($variables['text']); + + return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $safe_text . '</a>'); } } diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php index ba4719784865d18c5d1ba9f876c206cf774b79d1..142ce232ebf81900df48158d16f0ed4f5a51b11f 100644 --- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php +++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php @@ -18,16 +18,28 @@ interface LinkGeneratorInterface { /** * Renders a link to a URL. * + * Examples: + * @code + * $link_generator = \Drupal::service('link_generator'); + * $installer_url = \Drupal\Core\Url::fromUri('base://core/install.php'); + * $installer_link = $link_generator->generate($text, $installer_url); + * $external_url = \Drupal\Core\Url::fromUri('http://example.com', ['query' => ['foo' => 'bar']]); + * $external_link = $link_generator->generate($text, $external_url); + * $internal_url = \Drupal\Core\Url::fromRoute('system.admin'); + * $internal_link = $link_generator->generate($text, $internal_url); + * @endcode * However, for links enclosed in translatable text you should use t() and * embed the HTML anchor tag directly in the translated string. For example: * @code - * t('Visit the <a href="@url">content types</a> page', array('@url' => \Drupal::url('node.overview_types'))); + * $text = t('Visit the <a href="@url">content types</a> page', array('@url' => \Drupal::url('node.overview_types'))); * @endcode * This keeps the context of the link title ('settings' in the example) for * translators. * * @param string|array $text * The link text for the anchor tag as a translated string or render array. + * Strings will be sanitized automatically. If you need to output HTML in + * the link text you should use a render array. * @param \Drupal\Core\Url $url * The URL object used for the link. Amongst its options, the following may * be set to affect the generated link: @@ -36,11 +48,6 @@ interface LinkGeneratorInterface { * must be a string; other elements are more flexible, as they just need * to work as an argument for the constructor of the class * Drupal\Core\Template\Attribute($options['attributes']). - * - html: Whether $text is HTML or just plain-text. For - * example, to make an image tag into a link, this must be set to TRUE, or - * you will see the escaped HTML image tag. $text is not sanitized if - * 'html' is TRUE. The calling function must ensure that $text is already - * safe. Defaults to FALSE. * - language: An optional language object. If the path being linked to is * internal to the site, $options['language'] is used to determine whether * the link is "active", or pointing to the current page (the language as diff --git a/core/modules/aggregator/src/FeedViewBuilder.php b/core/modules/aggregator/src/FeedViewBuilder.php index 944d9a890269f0d1803ad0067914e8ebde826950..051ae36243474008e98e6c63be023859697559cf 100644 --- a/core/modules/aggregator/src/FeedViewBuilder.php +++ b/core/modules/aggregator/src/FeedViewBuilder.php @@ -103,7 +103,6 @@ public function buildComponents(array &$build, array $entities, array $displays, '#url' => Url::fromUri($link_href), '#options' => array( 'attributes' => array('class' => array('feed-image')), - 'html' => TRUE, ), ); } @@ -120,14 +119,16 @@ public function buildComponents(array &$build, array $entities, array $displays, if ($display->getComponent('more_link')) { $title_stripped = strip_tags($entity->label()); + $title = array( + '#type' => 'inline_template', + '#template' => '{% trans %}More<span class="visually-hidden"> posts about {{title}}</span>{% endtrans %}', + '#context' => array('title' => $title_stripped), + ); $build[$id]['more_link'] = array( '#type' => 'link', - '#title' => t('More<span class="visually-hidden"> posts about @title</span>', array( - '@title' => $title_stripped, - )), + '#title' => $title, '#url' => Url::fromRoute('entity.aggregator_feed.canonical', ['aggregator_feed' => $entity->id()]), '#options' => array( - 'html' => TRUE, 'attributes' => array( 'title' => $title_stripped, ), diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 85d295e4f1bacfe2098c1f928c3caf3c85432e2e..a155b2f5d8a58fd88bf2b885e5e771082201b509 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -42,7 +42,7 @@ function block_help($route_name, RouteMatchInterface $route_match) { $demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default'); $themes = list_themes(); $output = '<p>' . t('This page provides a drag-and-drop interface for adding a block to a region, and for controlling the order of blocks within regions. To add a block to a region, or to configure its specific title and visibility settings, click the block title under <em>Place blocks</em>. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') . '</p>'; - $output .= '<p>' . \Drupal::l(t('Demonstrate block regions (!theme)', array('!theme' => $themes[$demo_theme]->info['name'])), new Url('block.admin_demo', array('theme' => $demo_theme))) . '</p>'; + $output .= '<p>' . \Drupal::l(t('Demonstrate block regions (@theme)', array('@theme' => $themes[$demo_theme]->info['name'])), new Url('block.admin_demo', array('theme' => $demo_theme))) . '</p>'; return $output; } } diff --git a/core/modules/book/src/Tests/BookTest.php b/core/modules/book/src/Tests/BookTest.php index 44c71ec9260603033466eedc5a7a7736b17ed3e4..54c4bc13bcfb650183e73871ae1bcc9c03d28cea 100644 --- a/core/modules/book/src/Tests/BookTest.php +++ b/core/modules/book/src/Tests/BookTest.php @@ -207,24 +207,34 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F // Check previous, up, and next links. if ($previous) { + $text = array( + '#type' => 'inline_template', + '#template' => '<b>‹</b> {{ label }}', + '#context' => array('label' => $previous->label()), + ); /** @var \Drupal\Core\Url $url */ $url = $previous->urlInfo(); - $url->setOptions(array('html' => TRUE, 'attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page')))); - $this->assertRaw(\Drupal::l('<b>‹</b> ' . $previous->label(), $url), 'Previous page link found.'); + $url->setOptions(array('attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page')))); + $this->assertRaw(\Drupal::l($text, $url), 'Previous page link found.'); } if ($up) { /** @var \Drupal\Core\Url $url */ $url = $up->urlInfo(); - $url->setOptions(array('html'=> TRUE, 'attributes' => array('title' => t('Go to parent page')))); + $url->setOptions(array('attributes' => array('title' => t('Go to parent page')))); $this->assertRaw(\Drupal::l('Up', $url), 'Up page link found.'); } if ($next) { + $text = array( + '#type' => 'inline_template', + '#template' => '{{ label }} <b>›</b>', + '#context' => array('label' => $next->label()), + ); /** @var \Drupal\Core\Url $url */ $url = $next->urlInfo(); - $url->setOptions(array('html'=> TRUE, 'attributes' => array('rel' => array('next'), 'title' => t('Go to next page')))); - $this->assertRaw(\Drupal::l($next->label() . ' <b>›</b>', $url), 'Next page link found.'); + $url->setOptions(array('attributes' => array('rel' => array('next'), 'title' => t('Go to next page')))); + $this->assertRaw(\Drupal::l($text, $url), 'Next page link found.'); } // Compute the expected breadcrumb. diff --git a/core/modules/comment/comment.api.php b/core/modules/comment/comment.api.php index b460a7168b5e874bef59ddb62357bf383dc5a932..563cc5d4b8bde4f29bb6d9c73cd4144b3fa500cd 100644 --- a/core/modules/comment/comment.api.php +++ b/core/modules/comment/comment.api.php @@ -39,7 +39,6 @@ function hook_comment_links_alter(array &$links, CommentInterface $entity, array 'comment-report' => array( 'title' => t('Report'), 'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]), - 'html' => TRUE, ), ), ); diff --git a/core/modules/comment/src/CommentLinkBuilder.php b/core/modules/comment/src/CommentLinkBuilder.php index 228edff90ba5707450cf9ac4d437bcc7169abdd9..55664af1db3d3a9ed91f2f83825175cdb9e2d3a9 100644 --- a/core/modules/comment/src/CommentLinkBuilder.php +++ b/core/modules/comment/src/CommentLinkBuilder.php @@ -151,7 +151,6 @@ public function buildCommentedEntityLinks(FieldableEntityInterface $entity, arra elseif ($this->currentUser->isAnonymous()) { $links['comment-forbidden'] = array( 'title' => $this->commentManager->forbiddenMessage($entity, $field_name), - 'html' => TRUE, ); } } @@ -186,7 +185,6 @@ public function buildCommentedEntityLinks(FieldableEntityInterface $entity, arra elseif ($this->currentUser->isAnonymous()) { $links['comment-forbidden'] = array( 'title' => $this->commentManager->forbiddenMessage($entity, $field_name), - 'html' => TRUE, ); } } diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php index b903250a92a6f3c58cfc5eb6bf777aa91b1410f8..59b7ba7d5046cfc4795a86d7530fa9afe8ca8f4f 100644 --- a/core/modules/comment/src/CommentViewBuilder.php +++ b/core/modules/comment/src/CommentViewBuilder.php @@ -247,7 +247,6 @@ protected static function buildLinks(CommentInterface $entity, EntityInterface $ $links['comment-delete'] = array( 'title' => t('Delete'), 'url' => $entity->urlInfo('delete-form'), - 'html' => TRUE, ); } @@ -255,7 +254,6 @@ protected static function buildLinks(CommentInterface $entity, EntityInterface $ $links['comment-edit'] = array( 'title' => t('Edit'), 'url' => $entity->urlInfo('edit-form'), - 'html' => TRUE, ); } if ($entity->access('create')) { @@ -267,19 +265,16 @@ protected static function buildLinks(CommentInterface $entity, EntityInterface $ 'field_name' => $entity->getFieldName(), 'pid' => $entity->id(), ]), - 'html' => TRUE, ); } if (!$entity->isPublished() && $entity->access('approve')) { $links['comment-approve'] = array( 'title' => t('Approve'), 'url' => Url::fromRoute('comment.approve', ['comment' => $entity->id()]), - 'html' => TRUE, ); } if (empty($links) && \Drupal::currentUser()->isAnonymous()) { $links['comment-forbidden']['title'] = \Drupal::service('comment.manager')->forbiddenMessage($commented_entity, $entity->getFieldName()); - $links['comment-forbidden']['html'] = TRUE; } } @@ -288,7 +283,6 @@ protected static function buildLinks(CommentInterface $entity, EntityInterface $ $links['comment-translations'] = array( 'title' => t('Translate'), 'url' => $entity->urlInfo('drupal:content-translation-overview'), - 'html' => TRUE, ); } diff --git a/core/modules/comment/tests/modules/comment_test/comment_test.module b/core/modules/comment/tests/modules/comment_test/comment_test.module index 9fb600f6414a4d5dd39bad117d12f6c45834d334..d54814ba1c428c7015720c6c6648c03cd833c5c9 100644 --- a/core/modules/comment/tests/modules/comment_test/comment_test.module +++ b/core/modules/comment/tests/modules/comment_test/comment_test.module @@ -38,7 +38,6 @@ function comment_test_comment_links_alter(array &$links, CommentInterface &$enti 'comment-report' => array( 'title' => t('Report'), 'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]), - 'html' => TRUE, ), ), ); diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php index e8b1ba5dc07d31f88af6f5b554ac439c793d31a2..cbc1249b9747d10a713f1b20845d8ff20409bd5d 100644 --- a/core/modules/dblog/src/Controller/DbLogController.php +++ b/core/modules/dblog/src/Controller/DbLogController.php @@ -8,8 +8,9 @@ namespace Drupal\dblog\Controller; use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\Unicode; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; +use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Xss; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; @@ -175,14 +176,15 @@ public function overview() { foreach ($result as $dblog) { $message = $this->formatMessage($dblog); if ($message && isset($dblog->wid)) { - // Truncate link_text to 56 chars of message. - $log_text = Unicode::truncate(Xss::filter($message, array()), 56, TRUE, TRUE); + // Truncate link_text to 56 chars of message. This is a rare case where + // it is acceptable to call SafeMarkup::set() as we are truncating text + // that has already passed through SafeMarkup::set(). + $log_text = SafeMarkup::set(Unicode::truncate(Xss::filter($message, array()), 56, TRUE, TRUE)); $message = $this->l($log_text, new Url('dblog.event', array('event_id' => $dblog->wid), array( 'attributes' => array( // Provide a title for the link for useful hover hints. 'title' => Unicode::truncate(strip_tags($message), 256, TRUE, TRUE), ), - 'html' => TRUE, ))); } $username = array( diff --git a/core/modules/dblog/src/Tests/Views/ViewsIntegrationTest.php b/core/modules/dblog/src/Tests/Views/ViewsIntegrationTest.php index a8fcb5be1c77ad122f8f96f9e8753513e46475bf..3e3ede3d83020e38d5e1c570e5684735c31b27ac 100644 --- a/core/modules/dblog/src/Tests/Views/ViewsIntegrationTest.php +++ b/core/modules/dblog/src/Tests/Views/ViewsIntegrationTest.php @@ -77,7 +77,10 @@ public function testIntegration() { 'variables' => array( '@token1' => $this->randomMachineName(), '!token2' => $this->randomMachineName(), - 'link' => \Drupal::l('<object>Link</object>', new Url('<front>')), + 'link' => \Drupal::l(array( + '#type' => 'inline_template', + '#template' => '<object>Link</object>', + ), new Url('<front>')), ), ); $logger_factory = $this->container->get('logger.factory'); diff --git a/core/modules/field_ui/src/EntityDisplayModeListBuilder.php b/core/modules/field_ui/src/EntityDisplayModeListBuilder.php index 6056508ca467e41bf8031215e171098f7cf03715..453da32fb7396af5a1cbf420346c5cd1ef335bf3 100644 --- a/core/modules/field_ui/src/EntityDisplayModeListBuilder.php +++ b/core/modules/field_ui/src/EntityDisplayModeListBuilder.php @@ -121,9 +121,6 @@ public function render() { '#type' => 'link', '#url' => Url::fromRoute($short_type == 'view' ? 'field_ui.entity_view_mode_add_type' : 'field_ui.entity_form_mode_add_type', ['entity_type_id' => $entity_type]), '#title' => t('Add new %label @entity-type', array('%label' => $this->entityTypes[$entity_type]->getLabel(), '@entity-type' => $this->entityType->getLowercaseLabel())), - '#options' => array( - 'html' => TRUE, - ), ), 'colspan' => count($table['#header']), ); diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php index b73c06f482e7038096267eaa3a112a4dfddba6df..07aad4e30a9e69b73d4f1fce1256570cf2e41180 100644 --- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php +++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php @@ -8,6 +8,7 @@ namespace Drupal\image\Tests; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Url; use Drupal\field\Entity\FieldStorageConfig; /** @@ -26,6 +27,22 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { */ public static $modules = array('field_ui'); + /** + * The link generator. + * + * @var \Drupal\Core\Utility\LinkGeneratorInterface + */ + protected $linkGenerator; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->linkGenerator = $this->container->get('link_generator'); + } + /** * Test image formatters on node display for public files. */ @@ -108,7 +125,8 @@ function _testImageFieldFormatters($scheme) { '#width' => 40, '#height' => 20, ); - $default_output = '<a href="' . file_create_url($image_uri) . '">' . drupal_render($image) . '</a>'; + + $default_output = $this->linkGenerator->generate($image, Url::fromUri(file_create_url($image_uri))); $this->drupalGet('node/' . $nid); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index fd099a95b3ba76a897f2d204e4c16986f1d5c165..11f88f6a7aeddff1b1e61c1234bc39222c850aca 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -555,7 +555,6 @@ function hook_node_links_alter(array &$links, NodeInterface $entity, array &$con 'node-report' => array( 'title' => t('Report'), 'href' => "node/{$entity->id()}/report", - 'html' => TRUE, 'query' => array('token' => \Drupal::getContainer()->get('csrf_token')->get("node/{$entity->id()}/report")), ), ), diff --git a/core/modules/node/src/NodeViewBuilder.php b/core/modules/node/src/NodeViewBuilder.php index bcc580fdb8680eee12660a18a8a1826b3a602af8..e86e597f9841e12556e28015615bc8f38952cfff 100644 --- a/core/modules/node/src/NodeViewBuilder.php +++ b/core/modules/node/src/NodeViewBuilder.php @@ -149,7 +149,6 @@ protected static function buildLinks(NodeInterface $entity, $view_mode) { )), 'url' => $entity->urlInfo(), 'language' => $entity->language(), - 'html' => TRUE, 'attributes' => array( 'rel' => 'tag', 'title' => $node_title_stripped, diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 600ce7320a79b131f4931d5e3361860389b003db..402531a4e9e2014d4a999d752a74eb1a95673942 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -152,7 +152,6 @@ function theme_responsive_image_formatter($variables) { if (isset($variables['path']['path'])) { $path = $variables['path']['path']; $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); - $options['html'] = TRUE; return \Drupal::l($responsive_image, Url::fromUri($path, $options)); } diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php index 41e4f1c8f7d8ccd11accd967b7427adc774e13a5..7ac6609c4c008e3e923326f1f245532d504311e3 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -8,6 +8,7 @@ namespace Drupal\responsive_image\Tests; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Url; use Drupal\image\Tests\ImageFieldTestBase; /** @@ -34,11 +35,20 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { public static $modules = array('field_ui', 'responsive_image', 'responsive_image_test_module'); /** - * Drupal\simpletest\WebTestBase\setUp(). + * The link generator. + * + * @var \Drupal\Core\Utility\LinkGeneratorInterface + */ + protected $linkGenerator; + + /** + * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->linkGenerator = $this->container->get('link_generator'); + // Create user. $this->admin_user = $this->drupalCreateUser(array( 'administer responsive images', @@ -160,7 +170,8 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = '#width' => 40, '#height' => 20, ); - $default_output = '<a href="' . file_create_url($image_uri) . '">' . drupal_render($image) . '</a>'; + + $default_output = $this->linkGenerator->generate($image, Url::fromUri(file_create_url($image_uri))); $this->drupalGet('node/' . $nid); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index ba9c43db00553a76b080ad882c82de797a0ea3d4..a77c1020883723cc9b8c1685412c49b726fffc06 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -8,8 +8,8 @@ use Drupal\Core\Access\AccessResult; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Routing\UrlMatcher; use Drupal\Core\Url; use Drupal\shortcut\Entity\ShortcutSet; use Drupal\shortcut\ShortcutSetInterface; @@ -339,9 +339,13 @@ function shortcut_preprocess_page(&$variables) { ), '#prefix' => '<div class="add-or-remove-shortcuts ' . $link_mode . '-shortcut">', '#type' => 'link', - '#title' => '<span class="icon"></span><span class="text">'. $link_text .'</span>', + '#title' => array( + '#type' => 'inline_template', + '#template' => '<span class="icon"></span><span class="text">{{ link_text }}</span>', + '#context' => array('link_text' => $link_text), + ), '#url' => Url::fromRoute($route_name, $route_parameters), - '#options' => array('query' => $query, 'html' => TRUE), + '#options' => array('query' => $query), '#suffix' => '</div>', ); } diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index e114ebfcb170f9e39f23d8e3b2a05828755ffb5d..8b18f364408183bd950e75c4a336f44a6a20131d 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -294,6 +294,9 @@ protected function getAllOptions(\SimpleXMLElement $element) { * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertLink($label, $index = 0, $message = '', $group = 'Other') { + // $this->xpath will escape entities, so we need to decode them first + // to avoid double escaping leading to failed tests. + $label = html_entity_decode($label); $links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); $message = ($message ? $message : String::format('Link with label %label found.', array('%label' => $label))); return $this->assert(isset($links[$index]), $message, $group); diff --git a/core/modules/system/src/Tests/Theme/FunctionsTest.php b/core/modules/system/src/Tests/Theme/FunctionsTest.php index c1ad1c7e4da1ebfd7bf310d8de189719c1484a0c..d426b0fd8485b9d7e1fe061285f527ef9f26e0f2 100644 --- a/core/modules/system/src/Tests/Theme/FunctionsTest.php +++ b/core/modules/system/src/Tests/Theme/FunctionsTest.php @@ -211,6 +211,15 @@ function testLinks() { 'key' => 'value', ) ), + 'render array' => array( + 'title' => array( + '#type' => 'inline_template', + '#template' => '<span class="unescaped">{{ text }}</span>', + '#context' => array( + 'text' => 'potentially unsafe text that <should> be escaped', + ), + ), + ), ); $expected_links = ''; @@ -221,6 +230,7 @@ function testLinks() { $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>'; $query = array('key' => 'value'); $expected_links .= '<li class="query-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '">' . String::checkPlain('Query test route') . '</a></li>'; + $expected_links .= '<li class="render-array"><span class="unescaped">' . String::checkPlain('potentially unsafe text that <should> be escaped') . '</span></li>'; $expected_links .= '</ul>'; // Verify that passing a string as heading works. @@ -260,6 +270,7 @@ function testLinks() { $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>'; $query = array('key' => 'value'); $expected_links .= '<li class="query-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '">' . String::checkPlain('Query test route') . '</a></li>'; + $expected_links .= '<li class="render-array"><span class="unescaped">' . String::checkPlain('potentially unsafe text that <should> be escaped') . '</span></li>'; $expected_links .= '</ul>'; $expected = $expected_heading . $expected_links; $this->assertThemeOutput('links', $variables, $expected); @@ -276,6 +287,7 @@ function testLinks() { $query = array('key' => 'value'); $encoded_query = String::checkPlain(Json::encode($query)); $expected_links .= '<li data-drupal-link-query="'.$encoded_query.'" data-drupal-link-system-path="router_test/test1" class="query-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1', $query) . '" data-drupal-link-query="'.$encoded_query.'" data-drupal-link-system-path="router_test/test1">' . String::checkPlain('Query test route') . '</a></li>'; + $expected_links .= '<li class="render-array"><span class="unescaped">' . String::checkPlain('potentially unsafe text that <should> be escaped') . '</span></li>'; $expected_links .= '</ul>'; $expected = $expected_heading . $expected_links; $this->assertThemeOutput('links', $variables, $expected); diff --git a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module b/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module index 9d4e89f6370bb14965e7d3882cf2daa70a218e3b..f1c824128c4b7f4b92cbd34acc75ac9800af975f 100644 --- a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module +++ b/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module @@ -19,7 +19,6 @@ function toolbar_test_toolbar() { '#title' => t('Test tab'), '#url' => Url::fromRoute('<front>'), '#options' => array( - 'html' => FALSE, 'attributes' => array( 'id' => 'toolbar-tab-testing', 'title' => t('Test tab'), diff --git a/core/modules/user/user.module b/core/modules/user/user.module index a02d098776d4a1e55eedf2b4b1fd2d300c0f53e9..53e6c3a79b94cfcf09ef00979e6f11838051fcf7 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1425,7 +1425,6 @@ function user_toolbar() { 'account' => array( 'title' => t('View profile'), 'url' => Url::fromRoute('user.page'), - 'html' => TRUE, 'attributes' => array( 'title' => t('User account'), ), @@ -1433,7 +1432,6 @@ function user_toolbar() { 'account_edit' => array( 'title' => t('Edit profile'), 'url' => Url::fromRoute('entity.user.edit_form', ['user' => $user->id()]), - 'html' => TRUE, 'attributes' => array( 'title' => t('Edit user account'), ), diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 13ff1681cf13a32a104d93c2d434feeecf273ffd..59ff689cb92a3b851b823d9404464801d393b57c 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -1036,19 +1036,34 @@ public function overrideOption($option, $value) { * an easy URL to exactly the right section. Don't override this. */ public function optionLink($text, $section, $class = '', $title = '') { - if (!empty($class)) { - $text = '<span>' . $text . '</span>'; - } - if (!trim($text)) { $text = $this->t('Broken field'); } + if (!empty($class)) { + $text = [ + '#type' => 'inline_template', + '#template' => '<span>{{ text }}</span>', + '#context' => array('text' => $text), + ]; + } + if (empty($title)) { $title = $text; } - return \Drupal::l($text, new Url('views_ui.form_display', ['js' => 'nojs', 'view' => $this->view->storage->id(), 'display_id' => $this->display['id'], 'type' => $section], array('attributes' => array('class' => array('views-ajax-link', $class), 'title' => $title, 'id' => drupal_html_id('views-' . $this->display['id'] . '-' . $section)), 'html' => TRUE))); + return \Drupal::l($text, new Url('views_ui.form_display', array( + 'js' => 'nojs', + 'view' => $this->view->storage->id(), + 'display_id' => $this->display['id'], + 'type' => $section + ), array( + 'attributes' => array( + 'class' => array('views-ajax-link', $class), + 'title' => $title, + 'id' => drupal_html_id('views-' . $this->display['id'] . '-' . $section) + ) + ))); } /** @@ -1132,12 +1147,12 @@ public function optionsSummary(&$categories, &$options) { $options['display_id'] = array( 'category' => 'other', 'title' => $this->t('Machine Name'), - 'value' => !empty($this->display['new_id']) ? String::checkPlain($this->display['new_id']) : String::checkPlain($this->display['id']), + 'value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'], 'desc' => $this->t('Change the machine name of this display.'), ); } - $display_comment = String::checkPlain(Unicode::substr($this->getOption('display_comment'), 0, 10)); + $display_comment = Unicode::substr($this->getOption('display_comment'), 0, 10); $options['display_comment'] = array( 'category' => 'other', 'title' => $this->t('Administrative comment'), @@ -1331,7 +1346,7 @@ public function optionsSummary(&$categories, &$options) { $display_id = $this->getLinkDisplay(); $displays = $this->view->storage->get('display'); if (!empty($displays[$display_id])) { - $link_display = String::checkPlain($displays[$display_id]['display_title']); + $link_display = $displays[$display_id]['display_title']; } } @@ -1372,7 +1387,7 @@ public function optionsSummary(&$categories, &$options) { $options['exposed_form']['links']['exposed_form_options'] = $this->t('Exposed form settings for this exposed form style.'); } - $css_class = String::checkPlain(trim($this->getOption('css_class'))); + $css_class = trim($this->getOption('css_class')); if (!$css_class) { $css_class = $this->t('None'); } diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 4459ac2ef5bb0953fa9d0852f757671e44f9bf34..9c50a45ccec03f6b0814ae11fd4e6799cc5b7d20 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -1355,7 +1355,6 @@ protected function renderAsLink($alter, $text, $tokens) { } $options = array( - 'html' => TRUE, 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE, ); diff --git a/core/modules/views/src/Plugin/views/field/Url.php b/core/modules/views/src/Plugin/views/field/Url.php index cf66117749351901410b8a22234d647dc7c2dfac..381d17948298d9f931eca99a4a9e8f6ae0528df9 100644 --- a/core/modules/views/src/Plugin/views/field/Url.php +++ b/core/modules/views/src/Plugin/views/field/Url.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\field; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url as DrupalUrl; use Drupal\views\ResultRow; /** @@ -45,11 +46,14 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { public function render(ResultRow $values) { $value = $this->getValue($values); if (!empty($this->options['display_as_link'])) { - return _l($this->sanitizeValue($value), $value, array('html' => TRUE)); - } - else { - return $this->sanitizeValue($value, 'url'); + // If the URL is valid, render it normally. + if ($url = \Drupal::service('path.validator')->getUrlIfValidWithoutAccessCheck($value)) { + return \Drupal::l($this->sanitizeValue($value), $url); + } + // If the URL is not valid, treat it as an unrecognized local resource. + return \Drupal::l($this->sanitizeValue($value), DrupalUrl::fromUri('base://' . trim($value, '/'))); } + return $this->sanitizeValue($value, 'url'); } } diff --git a/core/modules/views/src/Tests/Handler/FieldUrlTest.php b/core/modules/views/src/Tests/Handler/FieldUrlTest.php index fe6d71af3083f0cb2ad4379859c17a2d7b4134f3..9215ca4273556391e43e09d42f1b0bee400d78cc 100644 --- a/core/modules/views/src/Tests/Handler/FieldUrlTest.php +++ b/core/modules/views/src/Tests/Handler/FieldUrlTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests\Handler; +use Drupal\Component\Utility\String; use Drupal\Core\Url; use Drupal\views\Tests\ViewUnitTestBase; use Drupal\views\Views; @@ -27,51 +28,92 @@ class FieldUrlTest extends ViewUnitTestBase { */ public static $testViews = array('test_view'); + /** + * Test URLs. + * + * @var array + * Associative array, keyed by test URL, with a boolean value indicating + * whether this is a valid URL. + */ + protected $urls = array( + 'http://www.drupal.org/' => TRUE, + '<front>' => TRUE, + 'admin' => TRUE, + '/admin' => TRUE, + 'some-non-existing-local-path' => FALSE, + '/some-non-existing-local-path' => FALSE, + '<script>alert("xss");</script>' => FALSE, + ); + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); $this->installSchema('system', 'url_alias'); } + /** + * {@inheritdoc} + */ function viewsData() { + // Reuse default data, changing the ID from a numeric field to a URL field. $data = parent::viewsData(); $data['views_test_data']['name']['field']['id'] = 'url'; return $data; } + /** + * {@inheritdoc} + */ + protected function dataSet() { + $dataset = array(); + foreach ($this->urls as $url => $valid) { + $dataset[] = array('name' => $url); + } + return $dataset; + } + + /** + * Tests the URL field handler. + */ public function testFieldUrl() { + $expected = array(); + foreach ($this->urls as $url => $valid) { + // In any case, the URL that is shown should always be properly escaped. + $text = String::checkPlain($url); + + // If the URL is not rendered as a link, it should just be shown as is. + $expected[FALSE][] = $text; + + // If the URL is rendered as a link and is a valid, it should be rendered + // normally. If it is not valid, it should be treated as a local resource. + $url = $valid ? \Drupal::service('path.validator')->getUrlIfValidWithoutAccessCheck($url) : Url::fromUri('base://' . trim($url, '/')); + $expected[TRUE][] = \Drupal::l($text, $url); + } + $view = Views::getView('test_view'); - $view->setDisplay(); - - $view->displayHandlers->get('default')->overrideOption('fields', array( - 'name' => array( - 'id' => 'name', - 'table' => 'views_test_data', - 'field' => 'name', - 'relationship' => 'none', - 'display_as_link' => FALSE, - ), - )); - - $this->executeView($view); - - $this->assertEqual('John', $view->field['name']->advancedRender($view->result[0])); - - // Make the url a link. - $view->destroy(); - $view->setDisplay(); - - $view->displayHandlers->get('default')->overrideOption('fields', array( - 'name' => array( - 'id' => 'name', - 'table' => 'views_test_data', - 'field' => 'name', - 'relationship' => 'none', - ), - )); - - $this->executeView($view); - - $this->assertEqual(\Drupal::l('John', Url::fromUri('base://John')), $view->field['name']->advancedRender($view->result[0])); + foreach ($expected as $display_as_link => $results) { + $view->setDisplay(); + + $view->displayHandlers->get('default')->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'display_as_link' => $display_as_link, + ), + )); + + $this->executeView($view); + + foreach ($results as $key => $result) { + $this->assertEqual($result, $view->field['name']->advancedRender($view->result[$key])); + } + + $view->destroy(); + } } } diff --git a/core/modules/views/src/Tests/Plugin/DisplayTest.php b/core/modules/views/src/Tests/Plugin/DisplayTest.php index 3c52272b3f1e058cf98de7dd94a47ac03cfb9453..5c8cc6bdd54aeaefdfa493d733bca5d1604c00d7 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests\Plugin; +use Drupal\Component\Utility\String; use Drupal\views\Views; use Drupal\views_test_data\Plugin\views\display\DisplayTest as DisplayTestPlugin; @@ -120,12 +121,12 @@ public function testDisplayPlugin() { $this->clickLink('Test option title'); - $this->randomString = $this->randomString(); - $this->drupalPostForm(NULL, array('test_option' => $this->randomString), t('Apply')); + $test_option = $this->randomString(); + $this->drupalPostForm(NULL, array('test_option' => $test_option), t('Apply')); // Check the new value has been saved by checking the UI summary text. $this->drupalGet('admin/structure/views/view/test_view/edit/display_test_1'); - $this->assertRaw($this->randomString); + $this->assertRaw(String::checkPlain($test_option)); // Test the enable/disable status of a display. $view->display_handler->setOption('enabled', FALSE); diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 4e007ad8f86733fb5fe3f5d57702ba1807a4b47f..183d20c4d7abec951fd19b60944922b2781773c8 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -484,7 +484,6 @@ function template_preprocess_views_view_table(&$variables) { $query['order'] = $field; $query['sort'] = $initial; $link_options = array( - 'html' => TRUE, 'attributes' => array('title' => $title), 'query' => $query, ); diff --git a/core/modules/views_ui/src/Form/Ajax/Rearrange.php b/core/modules/views_ui/src/Form/Ajax/Rearrange.php index cff75fdd4f2efc5acc7a60471e035a11ebfd2e85..ca213d19091cf04be69eb5ce9a42bb18687a549a 100644 --- a/core/modules/views_ui/src/Form/Ajax/Rearrange.php +++ b/core/modules/views_ui/src/Form/Ajax/Rearrange.php @@ -125,7 +125,19 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#id' => 'views-removed-' . $id, '#attributes' => array('class' => array('views-remove-checkbox')), '#default_value' => 0, - '#suffix' => \Drupal::l('<span>' . $this->t('Remove') . '</span>', Url::fromRoute('<none>', [], array('attributes' => array('id' => 'views-remove-link-' . $id, 'class' => array('views-hidden', 'views-button-remove', 'views-remove-link'), 'alt' => $this->t('Remove this item'), 'title' => $this->t('Remove this item')), 'html' => TRUE))), + '#suffix' => \Drupal::l( + array( + '#type' => 'inline_template', + '#template' => '<span>{{ text }}</span>', + '#context' => array('text' => $this->t('Remove')), + ), + Url::fromRoute('<none>', array(), array('attributes' => array( + 'id' => 'views-remove-link-' . $id, + 'class' => array('views-hidden', 'views-button-remove', 'views-remove-link'), + 'alt' => $this->t('Remove this item'), + 'title' => $this->t('Remove this item')), + )) + ), ); } diff --git a/core/modules/views_ui/src/Form/Ajax/ReorderDisplays.php b/core/modules/views_ui/src/Form/Ajax/ReorderDisplays.php index 72dae7c308a8e314faa9cffa7d8848e3307002d4..06be20360c9952246d007e84ed67746a2bfafd6d 100644 --- a/core/modules/views_ui/src/Form/Ajax/ReorderDisplays.php +++ b/core/modules/views_ui/src/Form/Ajax/ReorderDisplays.php @@ -120,11 +120,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { ), 'link' => array( '#type' => 'link', - '#title' => '<span>' . $this->t('Remove') . '</span>', - '#url' => Url::fromRoute('<none>'), - '#options' => array( - 'html' => TRUE, + '#title' => array( + '#type' => 'inline_template', + '#template' => '<span>{{ label }}</span>', + '#context' => array('label' => $this->t('Remove')), ), + '#url' => Url::fromRoute('<none>'), + '#href' => 'javascript:void()', '#attributes' => array( 'id' => 'display-remove-link-' . $id, 'class' => array('views-button-remove', 'display-remove-link'), diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php index 1ef346ba25cd10c6a8ab0c8c064004d4ebdba1e7..095f102cf9611937b2a6ce763d96e449afcf0dcc 100644 --- a/core/modules/views_ui/src/ViewEditForm.php +++ b/core/modules/views_ui/src/ViewEditForm.php @@ -985,7 +985,6 @@ public function getFormBucket(ViewUI $view, $type, $display) { 'title' => $add_text, 'url' => Url::fromRoute('views_ui.form_add_handler', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]), 'attributes' => array('class' => array('icon compact add', 'views-ajax-link'), 'id' => 'views-add-' . $type), - 'html' => TRUE, ); if ($count_handlers > 0) { // Create the rearrange text variable for the rearrange action. @@ -995,7 +994,6 @@ public function getFormBucket(ViewUI $view, $type, $display) { 'title' => $rearrange_text, 'url' => $rearrange_url, 'attributes' => array('class' => array($class, 'views-ajax-link'), 'id' => 'views-rearrange-' . $type), - 'html' => TRUE, ); } @@ -1057,7 +1055,7 @@ public function getFormBucket(ViewUI $view, $type, $display) { 'display_id' => $display['id'], 'type' => $type, 'id' => $id, - ), array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE))); + ), array('attributes' => array('class' => array('views-ajax-link'))))); continue; } @@ -1080,27 +1078,37 @@ public function getFormBucket(ViewUI $view, $type, $display) { 'display_id' => $display['id'], 'type' => $type, 'id' => $id, - ), array('attributes' => $link_attributes, 'html' => TRUE))); + ), array('attributes' => $link_attributes))); $build['fields'][$id]['#class'][] = Html::cleanCssIdentifier($display['id']. '-' . $type . '-' . $id); if ($executable->display_handler->useGroupBy() && $handler->usesGroupBy()) { - $build['fields'][$id]['#settings_links'][] = $this->l('<span class="label">' . $this->t('Aggregation settings') . '</span>', new Url('views_ui.form_handler_group', array( + $build['fields'][$id]['#settings_links'][] = $this->l(array( + '#type' => 'inline_template', + '#template' => '<span class="label">{{ label }}</span>', + '#context' => array('label' => $this->t('Aggregation settings')), + ), + new Url('views_ui.form_handler_group', array( 'js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type, 'id' => $id, - ), array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => $this->t('Aggregation settings')), 'html' => TRUE))); + ), array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => $this->t('Aggregation settings'))))); } if ($handler->hasExtraOptions()) { - $build['fields'][$id]['#settings_links'][] = $this->l('<span class="label">' . $this->t('Settings') . '</span>', new Url('views_ui.form_handler_extra', array( + $build['fields'][$id]['#settings_links'][] = $this->l(array( + '#type' => 'inline_template', + '#template' => '<span class="label">{{ label }}</span>', + '#context' => array('label' => $this->t('Settings')), + ), + new Url('views_ui.form_handler_extra', array( 'js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type, 'id' => $id, - ), array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => $this->t('Settings')), 'html' => TRUE))); + ), array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => $this->t('Settings'))))); } if ($grouping) { diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc index f56b3b5ee81a4d9a13bc7d9f9c336983f1677eef..b33f188ab5ca858d7113928a9088f6fc7e9b741e 100644 --- a/core/modules/views_ui/views_ui.theme.inc +++ b/core/modules/views_ui/views_ui.theme.inc @@ -158,7 +158,17 @@ function theme_views_ui_build_group_filter_form($variables) { 'value' => drupal_render($form['group_items'][$group_id]['value']), 'remove' => array( 'data' => array( - '#markup' => drupal_render($form['group_items'][$group_id]['remove']) . \Drupal::l('<span>' . t('Remove') . '</span>', Url::fromRoute('<none>', [], array('attributes' => array('id' => 'views-remove-link-' . $group_id, 'class' => array('views-hidden', 'views-button-remove', 'views-groups-remove-link', 'views-remove-link'), 'alt' => t('Remove this item'), 'title' => t('Remove this item')), 'html' => true))), + '#markup' => drupal_render($form['group_items'][$group_id]['remove']) . \Drupal::l( + array( + '#type' => 'inline_template', + '#template' => '<span>{% trans %}Remove{% endtrans %}</span>', + ), + Url::fromRoute('<none>', array(), array('attributes' => array( + 'id' => 'views-remove-link-' . $group_id, + 'class' => array('views-hidden', 'views-button-remove', 'views-groups-remove-link', 'views-remove-link'), + 'alt' => t('Remove this item'), + 'title' => t('Remove this item')), + ))), ), ), ); @@ -278,7 +288,10 @@ function template_preprocess_views_ui_rearrange_filter_form(&$variables) { $remove_link = array( '#type' => 'link', '#url' => Url::fromRoute('<none>'), - '#title' => '<span>' . t('Remove') . '</span>', + '#title' => array( + '#type' => 'inline_template', + '#template' => '<span>{% trans %}Remove{% endtrans %}</span>', + ), '#weight' => '1', '#options' => array( 'attributes' => array( @@ -292,7 +305,6 @@ function template_preprocess_views_ui_rearrange_filter_form(&$variables) { 'alt' => t('Remove this item'), 'title' => t('Remove this item'), ), - 'html' => TRUE, ), ); $row[]['data'] = array( diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php index 1f371fadf4324132043ca0f61a077a08b946265d..c5d47a60c68d4572982517fc890a44ab225285ad 100644 --- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Utility { +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Language\Language; use Drupal\Core\Url; use Drupal\Core\Utility\LinkGenerator; @@ -39,6 +40,13 @@ class LinkGeneratorTest extends UnitTestCase { */ protected $moduleHandler; + /** + * The mocked renderer. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $renderer; + /** * The mocked URL Assembler service. * @@ -51,7 +59,6 @@ class LinkGeneratorTest extends UnitTestCase { */ protected $defaultOptions = array( 'query' => array(), - 'html' => FALSE, 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE, @@ -65,8 +72,10 @@ protected function setUp() { $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE); $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface'); + + $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->renderer); - $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler); $this->urlAssembler = $this->getMock('\Drupal\Core\Utility\UnroutedUrlAssemblerInterface'); } @@ -325,7 +334,7 @@ public function testGenerateWithHtml() { )); $this->urlGenerator->expects($this->at(1)) ->method('generateFromRoute') - ->with('test_route_5', array(), array('html' => TRUE) + $this->defaultOptions) + ->with('test_route_5', array(), $this->defaultOptions) ->will($this->returnValue( '/test-route-5' )); @@ -344,10 +353,28 @@ public function testGenerateWithHtml() { ), ), $result); - // Test that the 'html' option allows unsanitized HTML link text. - $url = new Url('test_route_5', array(), array('html' => TRUE)); + // Test that HTML link text can be used in a render array. + $html = '<em>HTML output</em>'; + $render_array = [ + '#type' => 'inline_template', + '#template' => '<em>HTML output</em>', + ]; + + $this->renderer->expects($this->at(0)) + ->method('render') + ->with($render_array) + // Mark the HTML string as safe at the moment when the mocked render() + // method is invoked to mimic what occurs in render(). We cannot mock + // static methods. + ->will($this->returnCallback(function () use ($html) { + SafeMarkup::set($html); + return $html; + })); + + $url = new Url('test_route_5', array()); $url->setUrlGenerator($this->urlGenerator); - $result = $this->linkGenerator->generate('<em>HTML output</em>', $url); + + $result = $this->linkGenerator->generate($render_array, $url); $this->assertTag(array( 'tag' => 'a', 'attributes' => array('href' => '/test-route-5'),