Issue #2273923 by mpdonadio, pfrenssen, dawehner, xjm, cilefen: Remove html => TRUE option from l() and link generator

8.0.x
Alex Pott 2014-12-13 18:20:19 +01:00
parent f4108f88bf
commit 0bcabf50e0
39 changed files with 341 additions and 158 deletions

View File

@ -508,7 +508,7 @@ services:
arguments: ['@request_stack', '@config.factory' ] arguments: ['@request_stack', '@config.factory' ]
link_generator: link_generator:
class: Drupal\Core\Utility\LinkGenerator class: Drupal\Core\Utility\LinkGenerator
arguments: ['@url_generator', '@module_handler'] arguments: ['@url_generator', '@module_handler', '@renderer']
router: router:
class: Drupal\Core\Routing\AccessAwareRouter class: Drupal\Core\Routing\AccessAwareRouter
arguments: ['@router.no_access_checks', '@access_manager', '@current_user'] arguments: ['@router.no_access_checks', '@access_manager', '@current_user']

View File

@ -638,6 +638,8 @@ function drupal_http_header_attributes(array $attributes = array()) {
* *
* @param string|array $text * @param string|array $text
* The link text for the anchor tag as a translated string or render array. * 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 * @param string $path
* The internal path or external URL being linked to, such as "node/34" or * 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 * "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 * must be a string; other elements are more flexible, as they just need
* to work as an argument for the constructor of the class * to work as an argument for the constructor of the class
* Drupal\Core\Template\Attribute($options['attributes']). * 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 * - 'language': An optional language object. If the path being linked to is
* internal to the site, $options['language'] is used to determine whether * internal to the site, $options['language'] is used to determine whether
* the link is "active", or pointing to the current page (the language as * 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( $variables['options'] += array(
'attributes' => array(), 'attributes' => array(),
'query' => array(), 'query' => array(),
'html' => FALSE,
'language' => NULL, 'language' => NULL,
'set_active_class' => FALSE, '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. // in an HTML argument context, we need to encode it properly.
$url = String::checkPlain(_url($variables['path'], $variables['options'])); $url = String::checkPlain(_url($variables['path'], $variables['options']));
// Sanitize the link text if necessary. // Sanitize the link text.
$text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); $text = SafeMarkup::escape($variables['text']);
return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $text . '</a>'); return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $text . '</a>');
} }

View File

@ -327,26 +327,30 @@ function template_preprocess_menu_local_task(&$variables) {
$link += array( $link += array(
'localized_options' => array(), 'localized_options' => array(),
); );
$link_text = $link['title'];
if (!empty($variables['element']['#active'])) { if (!empty($variables['element']['#active'])) {
// Add text to indicate active tab for non-visual users. // Add text to indicate active tab for non-visual users.
$active = '<span class="visually-hidden">' . t('(active tab)') . '</span>';
$variables['attributes']['class'] = array('active'); $variables['attributes']['class'] = array('active');
// If the link does not contain HTML already, String::checkPlain() it now. // Build up an inline template which will be autoescaped.
// After we set 'html'=TRUE the link will not be sanitized by l(). $link_text = array(
if (empty($link['localized_options']['html'])) { '#type' => 'inline_template',
$link['title'] = String::checkPlain($link['title']); '#template' => '{{ title }}<span class="visually-hidden">{% trans %}(active tab){% endtrans %}></span>',
} '#context' => array('title' => $link['title']),
$link['localized_options']['html'] = TRUE; );
$link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active)); $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; $link['localized_options']['set_active_class'] = TRUE;
$variables['link'] = array( $variables['link'] = array(
'#type' => 'link', '#type' => 'link',
'#title' => $link_text, '#title' => $title,
'#url' => $link['url'], '#url' => $link['url'],
'#options' => $link['localized_options'], '#options' => $link['localized_options'],
); );

View File

@ -43,6 +43,11 @@ function tablesort_init($header) {
function tablesort_header(&$cell_content, array &$cell_attributes, array $header, array $ts) { function tablesort_header(&$cell_content, array &$cell_attributes, array $header, array $ts) {
// Special formatting for the currently sorted column header. // Special formatting for the currently sorted column header.
if (isset($cell_attributes['field'])) { if (isset($cell_attributes['field'])) {
$text = array(
'cell_content' => array(
'#markup' => $cell_content,
),
);
$title = t('sort by @s', array('@s' => $cell_content)); $title = t('sort by @s', array('@s' => $cell_content));
if ($cell_content == $ts['name']) { if ($cell_content == $ts['name']) {
// aria-sort is a WAI-ARIA property that indicates if items in a table // 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'; $cell_attributes['aria-sort'] = ($ts['sort'] == 'asc') ? 'ascending' : 'descending';
$ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc'); $ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc');
$cell_attributes['class'][] = 'active'; $cell_attributes['class'][] = 'active';
$tablesort_indicator = array(
'#theme' => 'tablesort_indicator',
'#style' => $ts['sort'],
);
$image = drupal_render($tablesort_indicator);
} }
else { 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'; $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), 'attributes' => array('title' => $title),
'query' => array_merge($ts['query'], array( 'query' => array_merge($ts['query'], array(
'sort' => $ts['sort'], 'sort' => $ts['sort'],
'order' => $cell_content, 'order' => $cell_content,
)), )),
'html' => TRUE,
])); ]));
unset($cell_attributes['field'], $cell_attributes['sort']); unset($cell_attributes['field'], $cell_attributes['sort']);

View File

@ -580,9 +580,6 @@ function template_preprocess_status_messages(&$variables) {
* - title: The link text. * - title: The link text.
* - url: (optional) The url object to link to. If omitted, no a tag is * - url: (optional) The url object to link to. If omitted, no a tag is
* printed out. * 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> * - attributes: (optional) Attributes for the anchor, or for the <span>
* tag used in its place if no 'href' is supplied. If element 'class' is * 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. * included, it must be an array of one or more class names.
@ -664,7 +661,7 @@ function template_preprocess_links(&$variables) {
$keys = ['title', 'url']; $keys = ['title', 'url'];
$link_element = array( $link_element = array(
'#type' => 'link', '#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)), '#options' => array_diff_key($link, array_combine($keys, $keys)),
'#url' => $link['url'], '#url' => $link['url'],
'#ajax' => $link['ajax'], '#ajax' => $link['ajax'],
@ -706,8 +703,7 @@ function template_preprocess_links(&$variables) {
} }
// Handle title-only text items. // Handle title-only text items.
$text = (!empty($link['html']) ? $link['title'] : String::checkPlain($link['title'])); $item['text'] = $link_element['#title'];
$item['text'] = $text;
if (isset($link['attributes'])) { if (isset($link['attributes'])) {
$item['text_attributes'] = new Attribute($link['attributes']); $item['text_attributes'] = new Attribute($link['attributes']);
} }

View File

@ -97,7 +97,6 @@ class Actions extends Container {
$button = drupal_render($element[$key]); $button = drupal_render($element[$key]);
$dropbuttons[$dropbutton]['#links'][$key] = array( $dropbuttons[$dropbutton]['#links'][$key] = array(
'title' => $button, 'title' => $button,
'html' => TRUE,
); );
} }
} }

View File

@ -12,7 +12,7 @@ use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Link; use Drupal\Core\Link;
use Drupal\Core\Path\AliasManagerInterface; use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Template\Attribute; use Drupal\Core\Template\Attribute;
use Drupal\Core\Url; use Drupal\Core\Url;
@ -36,6 +36,13 @@ class LinkGenerator implements LinkGeneratorInterface {
*/ */
protected $moduleHandler; protected $moduleHandler;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/** /**
* Constructs a LinkGenerator instance. * Constructs a LinkGenerator instance.
* *
@ -43,10 +50,13 @@ class LinkGenerator implements LinkGeneratorInterface {
* The url generator. * The url generator.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The 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->urlGenerator = $url_generator;
$this->moduleHandler = $module_handler; $this->moduleHandler = $module_handler;
$this->renderer = $renderer;
} }
/** /**
@ -75,8 +85,7 @@ class LinkGenerator implements LinkGeneratorInterface {
// Start building a structured representation of our link to be altered later. // Start building a structured representation of our link to be altered later.
$variables = array( $variables = array(
// @todo Inject the service when drupal_render() is converted to one. 'text' => is_array($text) ? $this->renderer->render($text) : $text,
'text' => is_array($text) ? drupal_render($text) : $text,
'url' => $url, 'url' => $url,
'options' => $url->getOptions(), 'options' => $url->getOptions(),
); );
@ -85,7 +94,6 @@ class LinkGenerator implements LinkGeneratorInterface {
$variables['options'] += array( $variables['options'] += array(
'attributes' => array(), 'attributes' => array(),
'query' => array(), 'query' => array(),
'html' => FALSE,
'language' => NULL, 'language' => NULL,
'set_active_class' => FALSE, 'set_active_class' => FALSE,
'absolute' => FALSE, 'absolute' => FALSE,
@ -135,9 +143,10 @@ class LinkGenerator implements LinkGeneratorInterface {
// it here in an HTML argument context, we need to encode it properly. // it here in an HTML argument context, we need to encode it properly.
$url = String::checkPlain($url->toString()); $url = String::checkPlain($url->toString());
// Sanitize the link text if necessary. // Make sure the link text is sanitized.
$text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); $safe_text = SafeMarkup::escape($variables['text']);
return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $text . '</a>');
return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $safe_text . '</a>');
} }
} }

View File

@ -18,16 +18,28 @@ interface LinkGeneratorInterface {
/** /**
* Renders a link to a URL. * 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 * However, for links enclosed in translatable text you should use t() and
* embed the HTML anchor tag directly in the translated string. For example: * embed the HTML anchor tag directly in the translated string. For example:
* @code * @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 * @endcode
* This keeps the context of the link title ('settings' in the example) for * This keeps the context of the link title ('settings' in the example) for
* translators. * translators.
* *
* @param string|array $text * @param string|array $text
* The link text for the anchor tag as a translated string or render array. * 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 * @param \Drupal\Core\Url $url
* The URL object used for the link. Amongst its options, the following may * The URL object used for the link. Amongst its options, the following may
* be set to affect the generated link: * 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 * must be a string; other elements are more flexible, as they just need
* to work as an argument for the constructor of the class * to work as an argument for the constructor of the class
* Drupal\Core\Template\Attribute($options['attributes']). * 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 * - language: An optional language object. If the path being linked to is
* internal to the site, $options['language'] is used to determine whether * internal to the site, $options['language'] is used to determine whether
* the link is "active", or pointing to the current page (the language as * the link is "active", or pointing to the current page (the language as

View File

@ -103,7 +103,6 @@ class FeedViewBuilder extends EntityViewBuilder {
'#url' => Url::fromUri($link_href), '#url' => Url::fromUri($link_href),
'#options' => array( '#options' => array(
'attributes' => array('class' => array('feed-image')), 'attributes' => array('class' => array('feed-image')),
'html' => TRUE,
), ),
); );
} }
@ -120,14 +119,16 @@ class FeedViewBuilder extends EntityViewBuilder {
if ($display->getComponent('more_link')) { if ($display->getComponent('more_link')) {
$title_stripped = strip_tags($entity->label()); $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( $build[$id]['more_link'] = array(
'#type' => 'link', '#type' => 'link',
'#title' => t('More<span class="visually-hidden"> posts about @title</span>', array( '#title' => $title,
'@title' => $title_stripped,
)),
'#url' => Url::fromRoute('entity.aggregator_feed.canonical', ['aggregator_feed' => $entity->id()]), '#url' => Url::fromRoute('entity.aggregator_feed.canonical', ['aggregator_feed' => $entity->id()]),
'#options' => array( '#options' => array(
'html' => TRUE,
'attributes' => array( 'attributes' => array(
'title' => $title_stripped, 'title' => $title_stripped,
), ),

View File

@ -42,7 +42,7 @@ function block_help($route_name, RouteMatchInterface $route_match) {
$demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default'); $demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default');
$themes = list_themes(); $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>' . 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; return $output;
} }
} }

View File

@ -207,24 +207,34 @@ class BookTest extends WebTestBase {
// Check previous, up, and next links. // Check previous, up, and next links.
if ($previous) { if ($previous) {
$text = array(
'#type' => 'inline_template',
'#template' => '<b></b> {{ label }}',
'#context' => array('label' => $previous->label()),
);
/** @var \Drupal\Core\Url $url */ /** @var \Drupal\Core\Url $url */
$url = $previous->urlInfo(); $url = $previous->urlInfo();
$url->setOptions(array('html' => TRUE, 'attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page')))); $url->setOptions(array('attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page'))));
$this->assertRaw(\Drupal::l('<b></b> ' . $previous->label(), $url), 'Previous page link found.'); $this->assertRaw(\Drupal::l($text, $url), 'Previous page link found.');
} }
if ($up) { if ($up) {
/** @var \Drupal\Core\Url $url */ /** @var \Drupal\Core\Url $url */
$url = $up->urlInfo(); $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.'); $this->assertRaw(\Drupal::l('Up', $url), 'Up page link found.');
} }
if ($next) { if ($next) {
$text = array(
'#type' => 'inline_template',
'#template' => '{{ label }} <b></b>',
'#context' => array('label' => $next->label()),
);
/** @var \Drupal\Core\Url $url */ /** @var \Drupal\Core\Url $url */
$url = $next->urlInfo(); $url = $next->urlInfo();
$url->setOptions(array('html'=> TRUE, 'attributes' => array('rel' => array('next'), 'title' => t('Go to next page')))); $url->setOptions(array('attributes' => array('rel' => array('next'), 'title' => t('Go to next page'))));
$this->assertRaw(\Drupal::l($next->label() . ' <b></b>', $url), 'Next page link found.'); $this->assertRaw(\Drupal::l($text, $url), 'Next page link found.');
} }
// Compute the expected breadcrumb. // Compute the expected breadcrumb.

View File

@ -39,7 +39,6 @@ function hook_comment_links_alter(array &$links, CommentInterface $entity, array
'comment-report' => array( 'comment-report' => array(
'title' => t('Report'), 'title' => t('Report'),
'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]), 'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]),
'html' => TRUE,
), ),
), ),
); );

View File

@ -151,7 +151,6 @@ class CommentLinkBuilder implements CommentLinkBuilderInterface {
elseif ($this->currentUser->isAnonymous()) { elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = array( $links['comment-forbidden'] = array(
'title' => $this->commentManager->forbiddenMessage($entity, $field_name), 'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
'html' => TRUE,
); );
} }
} }
@ -186,7 +185,6 @@ class CommentLinkBuilder implements CommentLinkBuilderInterface {
elseif ($this->currentUser->isAnonymous()) { elseif ($this->currentUser->isAnonymous()) {
$links['comment-forbidden'] = array( $links['comment-forbidden'] = array(
'title' => $this->commentManager->forbiddenMessage($entity, $field_name), 'title' => $this->commentManager->forbiddenMessage($entity, $field_name),
'html' => TRUE,
); );
} }
} }

View File

@ -247,7 +247,6 @@ class CommentViewBuilder extends EntityViewBuilder {
$links['comment-delete'] = array( $links['comment-delete'] = array(
'title' => t('Delete'), 'title' => t('Delete'),
'url' => $entity->urlInfo('delete-form'), 'url' => $entity->urlInfo('delete-form'),
'html' => TRUE,
); );
} }
@ -255,7 +254,6 @@ class CommentViewBuilder extends EntityViewBuilder {
$links['comment-edit'] = array( $links['comment-edit'] = array(
'title' => t('Edit'), 'title' => t('Edit'),
'url' => $entity->urlInfo('edit-form'), 'url' => $entity->urlInfo('edit-form'),
'html' => TRUE,
); );
} }
if ($entity->access('create')) { if ($entity->access('create')) {
@ -267,19 +265,16 @@ class CommentViewBuilder extends EntityViewBuilder {
'field_name' => $entity->getFieldName(), 'field_name' => $entity->getFieldName(),
'pid' => $entity->id(), 'pid' => $entity->id(),
]), ]),
'html' => TRUE,
); );
} }
if (!$entity->isPublished() && $entity->access('approve')) { if (!$entity->isPublished() && $entity->access('approve')) {
$links['comment-approve'] = array( $links['comment-approve'] = array(
'title' => t('Approve'), 'title' => t('Approve'),
'url' => Url::fromRoute('comment.approve', ['comment' => $entity->id()]), 'url' => Url::fromRoute('comment.approve', ['comment' => $entity->id()]),
'html' => TRUE,
); );
} }
if (empty($links) && \Drupal::currentUser()->isAnonymous()) { if (empty($links) && \Drupal::currentUser()->isAnonymous()) {
$links['comment-forbidden']['title'] = \Drupal::service('comment.manager')->forbiddenMessage($commented_entity, $entity->getFieldName()); $links['comment-forbidden']['title'] = \Drupal::service('comment.manager')->forbiddenMessage($commented_entity, $entity->getFieldName());
$links['comment-forbidden']['html'] = TRUE;
} }
} }
@ -288,7 +283,6 @@ class CommentViewBuilder extends EntityViewBuilder {
$links['comment-translations'] = array( $links['comment-translations'] = array(
'title' => t('Translate'), 'title' => t('Translate'),
'url' => $entity->urlInfo('drupal:content-translation-overview'), 'url' => $entity->urlInfo('drupal:content-translation-overview'),
'html' => TRUE,
); );
} }

View File

@ -38,7 +38,6 @@ function comment_test_comment_links_alter(array &$links, CommentInterface &$enti
'comment-report' => array( 'comment-report' => array(
'title' => t('Report'), 'title' => t('Report'),
'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]), 'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]),
'html' => TRUE,
), ),
), ),
); );

View File

@ -8,8 +8,9 @@
namespace Drupal\dblog\Controller; namespace Drupal\dblog\Controller;
use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection; use Drupal\Core\Database\Connection;
@ -175,14 +176,15 @@ class DbLogController extends ControllerBase {
foreach ($result as $dblog) { foreach ($result as $dblog) {
$message = $this->formatMessage($dblog); $message = $this->formatMessage($dblog);
if ($message && isset($dblog->wid)) { if ($message && isset($dblog->wid)) {
// Truncate link_text to 56 chars of message. // Truncate link_text to 56 chars of message. This is a rare case where
$log_text = Unicode::truncate(Xss::filter($message, array()), 56, TRUE, TRUE); // 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( $message = $this->l($log_text, new Url('dblog.event', array('event_id' => $dblog->wid), array(
'attributes' => array( 'attributes' => array(
// Provide a title for the link for useful hover hints. // Provide a title for the link for useful hover hints.
'title' => Unicode::truncate(strip_tags($message), 256, TRUE, TRUE), 'title' => Unicode::truncate(strip_tags($message), 256, TRUE, TRUE),
), ),
'html' => TRUE,
))); )));
} }
$username = array( $username = array(

View File

@ -77,7 +77,10 @@ class ViewsIntegrationTest extends ViewUnitTestBase {
'variables' => array( 'variables' => array(
'@token1' => $this->randomMachineName(), '@token1' => $this->randomMachineName(),
'!token2' => $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'); $logger_factory = $this->container->get('logger.factory');

View File

@ -121,9 +121,6 @@ class EntityDisplayModeListBuilder extends ConfigEntityListBuilder {
'#type' => 'link', '#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]), '#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())), '#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']), 'colspan' => count($table['#header']),
); );

View File

@ -8,6 +8,7 @@
namespace Drupal\image\Tests; namespace Drupal\image\Tests;
use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldStorageConfig;
/** /**
@ -26,6 +27,22 @@ class ImageFieldDisplayTest extends ImageFieldTestBase {
*/ */
public static $modules = array('field_ui'); 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. * Test image formatters on node display for public files.
*/ */
@ -108,7 +125,8 @@ class ImageFieldDisplayTest extends ImageFieldTestBase {
'#width' => 40, '#width' => 40,
'#height' => 20, '#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); $this->drupalGet('node/' . $nid);
$cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.');

View File

@ -555,7 +555,6 @@ function hook_node_links_alter(array &$links, NodeInterface $entity, array &$con
'node-report' => array( 'node-report' => array(
'title' => t('Report'), 'title' => t('Report'),
'href' => "node/{$entity->id()}/report", 'href' => "node/{$entity->id()}/report",
'html' => TRUE,
'query' => array('token' => \Drupal::getContainer()->get('csrf_token')->get("node/{$entity->id()}/report")), 'query' => array('token' => \Drupal::getContainer()->get('csrf_token')->get("node/{$entity->id()}/report")),
), ),
), ),

View File

@ -149,7 +149,6 @@ class NodeViewBuilder extends EntityViewBuilder {
)), )),
'url' => $entity->urlInfo(), 'url' => $entity->urlInfo(),
'language' => $entity->language(), 'language' => $entity->language(),
'html' => TRUE,
'attributes' => array( 'attributes' => array(
'rel' => 'tag', 'rel' => 'tag',
'title' => $node_title_stripped, 'title' => $node_title_stripped,

View File

@ -152,7 +152,6 @@ function theme_responsive_image_formatter($variables) {
if (isset($variables['path']['path'])) { if (isset($variables['path']['path'])) {
$path = $variables['path']['path']; $path = $variables['path']['path'];
$options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); $options = isset($variables['path']['options']) ? $variables['path']['options'] : array();
$options['html'] = TRUE;
return \Drupal::l($responsive_image, Url::fromUri($path, $options)); return \Drupal::l($responsive_image, Url::fromUri($path, $options));
} }

View File

@ -8,6 +8,7 @@
namespace Drupal\responsive_image\Tests; namespace Drupal\responsive_image\Tests;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\Core\Url;
use Drupal\image\Tests\ImageFieldTestBase; 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'); 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() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->linkGenerator = $this->container->get('link_generator');
// Create user. // Create user.
$this->admin_user = $this->drupalCreateUser(array( $this->admin_user = $this->drupalCreateUser(array(
'administer responsive images', 'administer responsive images',
@ -160,7 +170,8 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase {
'#width' => 40, '#width' => 40,
'#height' => 20, '#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); $this->drupalGet('node/' . $nid);
$cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.');

View File

@ -8,8 +8,8 @@
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\UrlMatcher;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\shortcut\Entity\ShortcutSet; use Drupal\shortcut\Entity\ShortcutSet;
use Drupal\shortcut\ShortcutSetInterface; use Drupal\shortcut\ShortcutSetInterface;
@ -339,9 +339,13 @@ function shortcut_preprocess_page(&$variables) {
), ),
'#prefix' => '<div class="add-or-remove-shortcuts ' . $link_mode . '-shortcut">', '#prefix' => '<div class="add-or-remove-shortcuts ' . $link_mode . '-shortcut">',
'#type' => 'link', '#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), '#url' => Url::fromRoute($route_name, $route_parameters),
'#options' => array('query' => $query, 'html' => TRUE), '#options' => array('query' => $query),
'#suffix' => '</div>', '#suffix' => '</div>',
); );
} }

View File

@ -294,6 +294,9 @@ trait AssertContentTrait {
* TRUE if the assertion succeeded, FALSE otherwise. * TRUE if the assertion succeeded, FALSE otherwise.
*/ */
protected function assertLink($label, $index = 0, $message = '', $group = 'Other') { 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)); $links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label));
$message = ($message ? $message : String::format('Link with label %label found.', array('%label' => $label))); $message = ($message ? $message : String::format('Link with label %label found.', array('%label' => $label)));
return $this->assert(isset($links[$index]), $message, $group); return $this->assert(isset($links[$index]), $message, $group);

View File

@ -211,6 +211,15 @@ class FunctionsTest extends WebTestBase {
'key' => 'value', '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 = ''; $expected_links = '';
@ -221,6 +230,7 @@ class FunctionsTest extends WebTestBase {
$expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>'; $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>';
$query = array('key' => 'value'); $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="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_links .= '</ul>';
// Verify that passing a string as heading works. // Verify that passing a string as heading works.
@ -260,6 +270,7 @@ class FunctionsTest extends WebTestBase {
$expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>'; $expected_links .= '<li class="router-test"><a href="' . \Drupal::urlGenerator()->generate('router_test.1') . '">' . String::checkPlain('Test route') . '</a></li>';
$query = array('key' => 'value'); $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="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_links .= '</ul>';
$expected = $expected_heading . $expected_links; $expected = $expected_heading . $expected_links;
$this->assertThemeOutput('links', $variables, $expected); $this->assertThemeOutput('links', $variables, $expected);
@ -276,6 +287,7 @@ class FunctionsTest extends WebTestBase {
$query = array('key' => 'value'); $query = array('key' => 'value');
$encoded_query = String::checkPlain(Json::encode($query)); $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 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_links .= '</ul>';
$expected = $expected_heading . $expected_links; $expected = $expected_heading . $expected_links;
$this->assertThemeOutput('links', $variables, $expected); $this->assertThemeOutput('links', $variables, $expected);

View File

@ -19,7 +19,6 @@ function toolbar_test_toolbar() {
'#title' => t('Test tab'), '#title' => t('Test tab'),
'#url' => Url::fromRoute('<front>'), '#url' => Url::fromRoute('<front>'),
'#options' => array( '#options' => array(
'html' => FALSE,
'attributes' => array( 'attributes' => array(
'id' => 'toolbar-tab-testing', 'id' => 'toolbar-tab-testing',
'title' => t('Test tab'), 'title' => t('Test tab'),

View File

@ -1425,7 +1425,6 @@ function user_toolbar() {
'account' => array( 'account' => array(
'title' => t('View profile'), 'title' => t('View profile'),
'url' => Url::fromRoute('user.page'), 'url' => Url::fromRoute('user.page'),
'html' => TRUE,
'attributes' => array( 'attributes' => array(
'title' => t('User account'), 'title' => t('User account'),
), ),
@ -1433,7 +1432,6 @@ function user_toolbar() {
'account_edit' => array( 'account_edit' => array(
'title' => t('Edit profile'), 'title' => t('Edit profile'),
'url' => Url::fromRoute('entity.user.edit_form', ['user' => $user->id()]), 'url' => Url::fromRoute('entity.user.edit_form', ['user' => $user->id()]),
'html' => TRUE,
'attributes' => array( 'attributes' => array(
'title' => t('Edit user account'), 'title' => t('Edit user account'),
), ),

View File

@ -1036,19 +1036,34 @@ abstract class DisplayPluginBase extends PluginBase {
* an easy URL to exactly the right section. Don't override this. * an easy URL to exactly the right section. Don't override this.
*/ */
public function optionLink($text, $section, $class = '', $title = '') { public function optionLink($text, $section, $class = '', $title = '') {
if (!empty($class)) {
$text = '<span>' . $text . '</span>';
}
if (!trim($text)) { if (!trim($text)) {
$text = $this->t('Broken field'); $text = $this->t('Broken field');
} }
if (!empty($class)) {
$text = [
'#type' => 'inline_template',
'#template' => '<span>{{ text }}</span>',
'#context' => array('text' => $text),
];
}
if (empty($title)) { if (empty($title)) {
$title = $text; $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 @@ abstract class DisplayPluginBase extends PluginBase {
$options['display_id'] = array( $options['display_id'] = array(
'category' => 'other', 'category' => 'other',
'title' => $this->t('Machine Name'), '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.'), '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( $options['display_comment'] = array(
'category' => 'other', 'category' => 'other',
'title' => $this->t('Administrative comment'), 'title' => $this->t('Administrative comment'),
@ -1331,7 +1346,7 @@ abstract class DisplayPluginBase extends PluginBase {
$display_id = $this->getLinkDisplay(); $display_id = $this->getLinkDisplay();
$displays = $this->view->storage->get('display'); $displays = $this->view->storage->get('display');
if (!empty($displays[$display_id])) { 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 @@ abstract class DisplayPluginBase extends PluginBase {
$options['exposed_form']['links']['exposed_form_options'] = $this->t('Exposed form settings for this exposed form style.'); $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) { if (!$css_class) {
$css_class = $this->t('None'); $css_class = $this->t('None');
} }

View File

@ -1355,7 +1355,6 @@ abstract class FieldPluginBase extends HandlerBase {
} }
$options = array( $options = array(
'html' => TRUE,
'absolute' => !empty($alter['absolute']) ? TRUE : FALSE, 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
); );

View File

@ -8,6 +8,7 @@
namespace Drupal\views\Plugin\views\field; namespace Drupal\views\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url as DrupalUrl;
use Drupal\views\ResultRow; use Drupal\views\ResultRow;
/** /**
@ -45,11 +46,14 @@ class Url extends FieldPluginBase {
public function render(ResultRow $values) { public function render(ResultRow $values) {
$value = $this->getValue($values); $value = $this->getValue($values);
if (!empty($this->options['display_as_link'])) { if (!empty($this->options['display_as_link'])) {
return _l($this->sanitizeValue($value), $value, array('html' => TRUE)); // If the URL is valid, render it normally.
} if ($url = \Drupal::service('path.validator')->getUrlIfValidWithoutAccessCheck($value)) {
else { return \Drupal::l($this->sanitizeValue($value), $url);
return $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');
} }
} }

View File

@ -7,6 +7,7 @@
namespace Drupal\views\Tests\Handler; namespace Drupal\views\Tests\Handler;
use Drupal\Component\Utility\String;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\views\Tests\ViewUnitTestBase; use Drupal\views\Tests\ViewUnitTestBase;
use Drupal\views\Views; use Drupal\views\Views;
@ -27,51 +28,92 @@ class FieldUrlTest extends ViewUnitTestBase {
*/ */
public static $testViews = array('test_view'); 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() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->installSchema('system', 'url_alias'); $this->installSchema('system', 'url_alias');
} }
/**
* {@inheritdoc}
*/
function viewsData() { function viewsData() {
// Reuse default data, changing the ID from a numeric field to a URL field.
$data = parent::viewsData(); $data = parent::viewsData();
$data['views_test_data']['name']['field']['id'] = 'url'; $data['views_test_data']['name']['field']['id'] = 'url';
return $data; 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() { 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 = Views::getView('test_view');
$view->setDisplay(); foreach ($expected as $display_as_link => $results) {
$view->setDisplay();
$view->displayHandlers->get('default')->overrideOption('fields', array( $view->displayHandlers->get('default')->overrideOption('fields', array(
'name' => array( 'name' => array(
'id' => 'name', 'id' => 'name',
'table' => 'views_test_data', 'table' => 'views_test_data',
'field' => 'name', 'field' => 'name',
'relationship' => 'none', 'relationship' => 'none',
'display_as_link' => FALSE, 'display_as_link' => $display_as_link,
), ),
)); ));
$this->executeView($view); $this->executeView($view);
$this->assertEqual('John', $view->field['name']->advancedRender($view->result[0])); foreach ($results as $key => $result) {
$this->assertEqual($result, $view->field['name']->advancedRender($view->result[$key]));
}
// Make the url a link. $view->destroy();
$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]));
} }
} }

View File

@ -7,6 +7,7 @@
namespace Drupal\views\Tests\Plugin; namespace Drupal\views\Tests\Plugin;
use Drupal\Component\Utility\String;
use Drupal\views\Views; use Drupal\views\Views;
use Drupal\views_test_data\Plugin\views\display\DisplayTest as DisplayTestPlugin; use Drupal\views_test_data\Plugin\views\display\DisplayTest as DisplayTestPlugin;
@ -120,12 +121,12 @@ class DisplayTest extends PluginTestBase {
$this->clickLink('Test option title'); $this->clickLink('Test option title');
$this->randomString = $this->randomString(); $test_option = $this->randomString();
$this->drupalPostForm(NULL, array('test_option' => $this->randomString), t('Apply')); $this->drupalPostForm(NULL, array('test_option' => $test_option), t('Apply'));
// Check the new value has been saved by checking the UI summary text. // 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->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. // Test the enable/disable status of a display.
$view->display_handler->setOption('enabled', FALSE); $view->display_handler->setOption('enabled', FALSE);

View File

@ -484,7 +484,6 @@ function template_preprocess_views_view_table(&$variables) {
$query['order'] = $field; $query['order'] = $field;
$query['sort'] = $initial; $query['sort'] = $initial;
$link_options = array( $link_options = array(
'html' => TRUE,
'attributes' => array('title' => $title), 'attributes' => array('title' => $title),
'query' => $query, 'query' => $query,
); );

View File

@ -125,7 +125,19 @@ class Rearrange extends ViewsFormBase {
'#id' => 'views-removed-' . $id, '#id' => 'views-removed-' . $id,
'#attributes' => array('class' => array('views-remove-checkbox')), '#attributes' => array('class' => array('views-remove-checkbox')),
'#default_value' => 0, '#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')),
))
),
); );
} }

View File

@ -120,11 +120,13 @@ class ReorderDisplays extends ViewsFormBase {
), ),
'link' => array( 'link' => array(
'#type' => 'link', '#type' => 'link',
'#title' => '<span>' . $this->t('Remove') . '</span>', '#title' => array(
'#url' => Url::fromRoute('<none>'), '#type' => 'inline_template',
'#options' => array( '#template' => '<span>{{ label }}</span>',
'html' => TRUE, '#context' => array('label' => $this->t('Remove')),
), ),
'#url' => Url::fromRoute('<none>'),
'#href' => 'javascript:void()',
'#attributes' => array( '#attributes' => array(
'id' => 'display-remove-link-' . $id, 'id' => 'display-remove-link-' . $id,
'class' => array('views-button-remove', 'display-remove-link'), 'class' => array('views-button-remove', 'display-remove-link'),

View File

@ -985,7 +985,6 @@ class ViewEditForm extends ViewFormBase {
'title' => $add_text, 'title' => $add_text,
'url' => Url::fromRoute('views_ui.form_add_handler', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]), '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), 'attributes' => array('class' => array('icon compact add', 'views-ajax-link'), 'id' => 'views-add-' . $type),
'html' => TRUE,
); );
if ($count_handlers > 0) { if ($count_handlers > 0) {
// Create the rearrange text variable for the rearrange action. // Create the rearrange text variable for the rearrange action.
@ -995,7 +994,6 @@ class ViewEditForm extends ViewFormBase {
'title' => $rearrange_text, 'title' => $rearrange_text,
'url' => $rearrange_url, 'url' => $rearrange_url,
'attributes' => array('class' => array($class, 'views-ajax-link'), 'id' => 'views-rearrange-' . $type), 'attributes' => array('class' => array($class, 'views-ajax-link'), 'id' => 'views-rearrange-' . $type),
'html' => TRUE,
); );
} }
@ -1057,7 +1055,7 @@ class ViewEditForm extends ViewFormBase {
'display_id' => $display['id'], 'display_id' => $display['id'],
'type' => $type, 'type' => $type,
'id' => $id, 'id' => $id,
), array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE))); ), array('attributes' => array('class' => array('views-ajax-link')))));
continue; continue;
} }
@ -1080,27 +1078,37 @@ class ViewEditForm extends ViewFormBase {
'display_id' => $display['id'], 'display_id' => $display['id'],
'type' => $type, 'type' => $type,
'id' => $id, 'id' => $id,
), array('attributes' => $link_attributes, 'html' => TRUE))); ), array('attributes' => $link_attributes)));
$build['fields'][$id]['#class'][] = Html::cleanCssIdentifier($display['id']. '-' . $type . '-' . $id); $build['fields'][$id]['#class'][] = Html::cleanCssIdentifier($display['id']. '-' . $type . '-' . $id);
if ($executable->display_handler->useGroupBy() && $handler->usesGroupBy()) { 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', 'js' => 'nojs',
'view' => $view->id(), 'view' => $view->id(),
'display_id' => $display['id'], 'display_id' => $display['id'],
'type' => $type, 'type' => $type,
'id' => $id, '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()) { 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', 'js' => 'nojs',
'view' => $view->id(), 'view' => $view->id(),
'display_id' => $display['id'], 'display_id' => $display['id'],
'type' => $type, 'type' => $type,
'id' => $id, '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) { if ($grouping) {

View File

@ -158,7 +158,17 @@ function theme_views_ui_build_group_filter_form($variables) {
'value' => drupal_render($form['group_items'][$group_id]['value']), 'value' => drupal_render($form['group_items'][$group_id]['value']),
'remove' => array( 'remove' => array(
'data' => 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( $remove_link = array(
'#type' => 'link', '#type' => 'link',
'#url' => Url::fromRoute('<none>'), '#url' => Url::fromRoute('<none>'),
'#title' => '<span>' . t('Remove') . '</span>', '#title' => array(
'#type' => 'inline_template',
'#template' => '<span>{% trans %}Remove{% endtrans %}</span>',
),
'#weight' => '1', '#weight' => '1',
'#options' => array( '#options' => array(
'attributes' => array( 'attributes' => array(
@ -292,7 +305,6 @@ function template_preprocess_views_ui_rearrange_filter_form(&$variables) {
'alt' => t('Remove this item'), 'alt' => t('Remove this item'),
'title' => t('Remove this item'), 'title' => t('Remove this item'),
), ),
'html' => TRUE,
), ),
); );
$row[]['data'] = array( $row[]['data'] = array(

View File

@ -7,6 +7,7 @@
namespace Drupal\Tests\Core\Utility { namespace Drupal\Tests\Core\Utility {
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGenerator; use Drupal\Core\Utility\LinkGenerator;
@ -39,6 +40,13 @@ class LinkGeneratorTest extends UnitTestCase {
*/ */
protected $moduleHandler; protected $moduleHandler;
/**
* The mocked renderer.
*
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $renderer;
/** /**
* The mocked URL Assembler service. * The mocked URL Assembler service.
* *
@ -51,7 +59,6 @@ class LinkGeneratorTest extends UnitTestCase {
*/ */
protected $defaultOptions = array( protected $defaultOptions = array(
'query' => array(), 'query' => array(),
'html' => FALSE,
'language' => NULL, 'language' => NULL,
'set_active_class' => FALSE, 'set_active_class' => FALSE,
'absolute' => FALSE, 'absolute' => FALSE,
@ -65,8 +72,10 @@ class LinkGeneratorTest extends UnitTestCase {
$this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE); $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE);
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); $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'); $this->urlAssembler = $this->getMock('\Drupal\Core\Utility\UnroutedUrlAssemblerInterface');
} }
@ -325,7 +334,7 @@ class LinkGeneratorTest extends UnitTestCase {
)); ));
$this->urlGenerator->expects($this->at(1)) $this->urlGenerator->expects($this->at(1))
->method('generateFromRoute') ->method('generateFromRoute')
->with('test_route_5', array(), array('html' => TRUE) + $this->defaultOptions) ->with('test_route_5', array(), $this->defaultOptions)
->will($this->returnValue( ->will($this->returnValue(
'/test-route-5' '/test-route-5'
)); ));
@ -344,10 +353,28 @@ class LinkGeneratorTest extends UnitTestCase {
), ),
), $result); ), $result);
// Test that the 'html' option allows unsanitized HTML link text. // Test that HTML link text can be used in a render array.
$url = new Url('test_route_5', array(), array('html' => TRUE)); $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); $url->setUrlGenerator($this->urlGenerator);
$result = $this->linkGenerator->generate('<em>HTML output</em>', $url);
$result = $this->linkGenerator->generate($render_array, $url);
$this->assertTag(array( $this->assertTag(array(
'tag' => 'a', 'tag' => 'a',
'attributes' => array('href' => '/test-route-5'), 'attributes' => array('href' => '/test-route-5'),