From ded07e9aa86b6a77b88801500e76295fd972188f Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Fri, 30 Aug 2013 16:28:49 +0100 Subject: [PATCH] Issue #2047619 by dawehner, pwolanin, tstoeckler, thedavidmeister: Add a link generator service for route-based links. --- core/core.services.yml | 5 + core/includes/common.inc | 22 +- core/lib/Drupal.php | 9 + .../Drupal/Core/Controller/ControllerBase.php | 54 +++ core/lib/Drupal/Core/Routing/UrlGenerator.php | 2 +- .../lib/Drupal/Core/Utility/LinkGenerator.php | 152 +++++++ .../Core/Utility/LinkGeneratorInterface.php | 66 +++ .../dblog/Controller/DbLogController.php | 5 +- core/modules/system/system.api.php | 50 +++ .../views_ui/Controller/ViewsUIController.php | 24 +- .../Tests/Core/Utility/LinkGeneratorTest.php | 407 ++++++++++++++++++ 11 files changed, 783 insertions(+), 13 deletions(-) create mode 100644 core/lib/Drupal/Core/Utility/LinkGenerator.php create mode 100644 core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php create mode 100644 core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 8a24ee77ae9e..f941e333fabd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -242,6 +242,11 @@ services: - [setContext, ['@?router.request_context']] tags: - { name: persist } + link_generator: + class: Drupal\Core\Utility\LinkGenerator + arguments: ['@url_generator', '@module_handler', '@language_manager'] + calls: + - [setRequest, ['@?request']] router.dynamic: class: Symfony\Cmf\Component\Routing\DynamicRouter arguments: ['@router.request_context', '@router.matcher', '@url_generator'] diff --git a/core/includes/common.inc b/core/includes/common.inc index c6f6688dff47..d6886f2fd3d9 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1266,6 +1266,12 @@ function drupal_http_header_attributes(array $attributes = array()) { * This keeps the context of the link title ('settings' in the example) for * translators. * + * This function does not support generating links from internal routes. For + * that use \Drupal\Core\Utility\LinkGenerator::generate(), which is exposed via + * the 'link_generator' service. It requires an internal route name and does not + * support external URLs. Using Drupal 7 style system paths should be avoided if + * possible but l() should still be used when rendering links to external URLs. + * * @param string|array $text * The link text for the anchor tag as a translated string or render array. * @param string $path @@ -3471,8 +3477,12 @@ function drupal_pre_render_html_tag($element) { * @param $elements * A structured array whose keys form the arguments to l(): * - #title: The link text to pass as argument to l(). - * - #href: The URL path component to pass as argument to l(). - * - #options: (optional) An array of options to pass to l(). + * - One of the following + * - #route_name and (optionally) and a #route_parameters array; The route + * name and route parameters which will be passed into the link generator. + * - #href: The system path or URL to pass as argument to l(). + * - #options: (optional) An array of options to pass to l() or the link + * generator. * * @return * The passed-in elements containing a rendered link in '#markup'. @@ -3513,7 +3523,13 @@ function drupal_pre_render_link($element) { $element = ajax_pre_render_element($element); } - $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); + if (isset($element['#route_name'])) { + $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters']; + $element['#markup'] = Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']); + } + else { + $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); + } return $element; } diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 0d9ed964aeae..04fac027ca61 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -380,6 +380,15 @@ public static function urlGenerator() { return static::$container->get('url_generator'); } + /** + * Returns the link generator service. + * + * @return \Drupal\Core\Utility\LinkGeneratorInterface + */ + public static function linkGenerator() { + return static::$container->get('link_generator'); + } + /** * Returns the string translation service. * diff --git a/core/lib/Drupal/Core/Controller/ControllerBase.php b/core/lib/Drupal/Core/Controller/ControllerBase.php index d0718ff33667..7209c40d5e31 100644 --- a/core/lib/Drupal/Core/Controller/ControllerBase.php +++ b/core/lib/Drupal/Core/Controller/ControllerBase.php @@ -121,6 +121,60 @@ protected function urlGenerator() { return $this->container->get('url_generator'); } + /** + * Renders a link to a route given a route name and its parameters. + * + * This function correctly handles aliased paths and sanitizing text, so all + * internal links output by modules should be generated by this function if + * possible. + * + * 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::urlGenerator()->generate('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. + * @param string $route_name + * The name of the route to use to generate the link. + * @param array $parameters + * (optional) Any parameters needed to render the route path pattern. + * @param array $options + * (optional) An associative array of additional options. Defaults to an + * empty array. It may contain the following elements: + * - 'query': An array of query key/value-pairs (without any URL-encoding) to + * append to the URL. + * - absolute: Whether to force the output to be an absolute link (beginning + * with http:). Useful for links that will be displayed outside the site, + * such as in an RSS feed. Defaults to FALSE. + * - attributes: An associative array of HTML attributes to apply to the + * anchor tag. If element 'class' is included, it must be an array; 'title' + * 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 + * well as the path must match). + * + * @return string + * An HTML string containing a link to the given route and parameters. + * + * @see \Drupal\Core\Routing\UrlGenerator::generateFromRoute() + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function l($text, $route_name, array $parameters = array(), array $options = array()) { + return $this->container->get('link_generator')->generate($text, $route_name, $parameters, $options); + } + /** * Returns the current user. * diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index ac5d5c8beccf..53ac95dbb38a 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -21,7 +21,7 @@ use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; /** - * A Generator creates URL strings based on a specified route. + * Defines an interface which generates a link with route names and parameters. */ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterface { diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php new file mode 100644 index 000000000000..c2a249c8e6ed --- /dev/null +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -0,0 +1,152 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Utility\LinkGenerator. + */ + +namespace Drupal\Core\Utility; + +use Drupal\Component\Utility\String; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Routing\UrlGeneratorInterface; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Provides a class which generates a link with route names and parameters. + */ +class LinkGenerator implements LinkGeneratorInterface { + + /** + * Stores some information about the current request, like the language. + * + * @var array + */ + protected $active; + + /** + * The url generator. + * + * @var \Drupal\Core\Routing\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * The module handler firing the route_link alter hook. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + + /** + * Constructs a LinkGenerator instance. + * + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The url generator. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. + */ + public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerInterface $module_handler, LanguageManager $language_manager) { + $this->urlGenerator = $url_generator; + $this->moduleHandler = $module_handler; + $this->languageManager = $language_manager; + } + + /** + * Sets the $request property. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + */ + public function setRequest(Request $request) { + // Pre-calculate and store values based on the request that may be used + // repeatedly in generate(). + $this->active = array( + 'route_name' => $request->attributes->get(RouteObjectInterface::ROUTE_NAME), + 'language' => $this->languageManager->getLanguage(Language::TYPE_URL)->id, + 'parameters' => (array) $request->attributes->get('_raw_variables') + (array) $request->query->all(), + ); + } + + /** + * {@inheritdoc} + */ + public function generate($text, $route_name, array $parameters = array(), array $options = array()) { + // 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, + 'route_name' => $route_name, + 'parameters' => $parameters, + 'options' => $options, + ); + + // Merge in default options. + $variables['options'] += array( + 'attributes' => array(), + 'query' => array(), + 'html' => FALSE, + 'language' => NULL, + ); + // Add a hreflang attribute if we know the language of this link's url and + // hreflang has not already been set. + if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) { + $variables['options']['attributes']['hreflang'] = $variables['options']['language']->id; + } + + // This is only needed for the active class. The generator also combines + // the parameters and $options['query'] and adds parameters that are not + // path slugs as query strings. + $full_parameters = $parameters + (array) $variables['options']['query']; + + // Determine whether this link is "active", meaning that it has the same + // URL path and query string as the current page. Note that this may be + // removed from l() in https://drupal.org/node/1979468 and would be removed + // or altered here also. + $variables['url_is_active'] = $route_name == $this->active['route_name'] + // The language of an active link is equal to the current language. + && (empty($variables['options']['language']) || $variables['options']['language']->id == $this->active['language']) + && $full_parameters == $this->active['parameters']; + + // Add the "active" class if appropriate. + if ($variables['url_is_active']) { + $variables['options']['attributes']['class'][] = 'active'; + } + + // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags() + // only when a quick strpos() gives suspicion tags are present. + if (isset($variables['options']['attributes']['title']) && strpos($variables['options']['attributes']['title'], '<') !== FALSE) { + $variables['options']['attributes']['title'] = strip_tags($variables['options']['attributes']['title']); + } + + // Allow other modules to modify the structure of the link. + $this->moduleHandler->alter('link', $variables); + + // Move attributes out of options. generateFromRoute(() doesn't need them. + $attributes = new Attribute($variables['options']['attributes']); + unset($variables['options']['attributes']); + + // The result of the url generator is a plain-text URL. Because we are using + // it here in an HTML argument context, we need to encode it properly. + $url = String::checkPlain($this->urlGenerator->generateFromRoute($variables['route_name'], $variables['parameters'], $variables['options'])); + + // Sanitize the link text if necessary. + $text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); + + return '<a href="' . $url . '"' . $attributes . '>' . $text . '</a>'; + } + +} diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php new file mode 100644 index 000000000000..2157a5fbd2a7 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php @@ -0,0 +1,66 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Utility\LinkGeneratorInterface. + */ + +namespace Drupal\Core\Utility; + +/** + * Defines an interface for a service which generates a link out of a + */ +interface LinkGeneratorInterface { + + /** + * Renders a link to a route given a route name and its parameters. + * + * This function correctly handles aliased paths and sanitizing text, so all + * internal links output by modules should be generated by this function if + * possible. + * + * 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::urlGenerator()->generate('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. + * @param string $route_name + * The name of the route to use to generate the link. + * @param array $parameters + * (optional) Any parameters needed to render the route path pattern. + * @param array $options + * (optional) An associative array of additional options. Defaults to an + * empty array. It may contain the following elements: + * - 'query': An array of query key/value-pairs (without any URL-encoding) to + * append to the URL. + * - absolute: Whether to force the output to be an absolute link (beginning + * with http:). Useful for links that will be displayed outside the site, + * such as in an RSS feed. Defaults to FALSE. + * - attributes: An associative array of HTML attributes to apply to the + * anchor tag. If element 'class' is included, it must be an array; 'title' + * 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 + * well as the path must match). + * + * @return string + * An HTML string containing a link to the given route and parameters. + * + * @see \Drupal\Core\Routing\UrlGenerator::generateFromRoute() + */ + public function generate($text, $route_name, array $parameters = array(), array $options = array()); + +} diff --git a/core/modules/dblog/lib/Drupal/dblog/Controller/DbLogController.php b/core/modules/dblog/lib/Drupal/dblog/Controller/DbLogController.php index a0103635c868..5123f04f49c7 100644 --- a/core/modules/dblog/lib/Drupal/dblog/Controller/DbLogController.php +++ b/core/modules/dblog/lib/Drupal/dblog/Controller/DbLogController.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; +use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; use Drupal\Core\Controller\ControllerInterface; use Drupal\Core\Datetime\Date; @@ -20,7 +21,7 @@ /** * Returns responses for dblog routes. */ -class DbLogController implements ControllerInterface { +class DbLogController extends ControllerBase implements ControllerInterface { /** * The database service. @@ -176,7 +177,7 @@ public function overview() { if (isset($dblog->wid)) { // Truncate link_text to 56 chars of message. $log_text = Unicode::truncate(filter_xss($message, array()), 56, TRUE, TRUE); - $message = l($log_text, 'admin/reports/event/' . $dblog->wid, array('html' => TRUE)); + $message = $this->l($log_text, 'dblog_event', array('event_id' => $dblog->wid), array('html' => TRUE)); } } $username = array( diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 6339d1c17cde..49e87a08db55 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -3489,6 +3489,56 @@ function hook_filetransfer_info_alter(&$filetransfer_info) { } } +/** + * Alter the parameters for links. + * + * @param array $variables + * An associative array of variables defining a link. The link may be either a + * "route link" using \Drupal\Core\Utility\LinkGenerator::link(), which is + * exposed as the 'link_generator' service or a link generated by l(). If the + * link is a "route link", 'route_name' will be set, otherwise 'path' will be + * set. The following keys can be altered: + * - text: The link text for the anchor tag as a translated string. + * - url_is_active: Whether or not the link points to the currently active + * URL. + * - path: If this link is being generated by l(), this system path, relative + * path, or external URL will be passed to url() to generate the href + * attribute for this link. + * - route_name: The name of the route to use to generate the link, if + * this is a "route link". + * - parameters: Any parameters needed to render the route path pattern, if + * this is a "route link". + * - options: An associative array of additional options that will be passed + * to either \Drupal\Core\Routing\UrlGenerator::generateFromPath() or + * \Drupal\Core\Routing\UrlGenerator::generateFromRoute() to generate the + * href attribute for this link, and also used when generating the link. + * Defaults to an empty array. It may contain the following elements: + * - 'query': An array of query key/value-pairs (without any URL-encoding) to + * append to the URL. + * - absolute: Whether to force the output to be an absolute link (beginning + * with http:). Useful for links that will be displayed outside the site, + * such as in an RSS feed. Defaults to FALSE. + * - language: An optional language object. May affect the rendering of + * the anchor tag, such as by adding a language prefix to the path. + * - attributes: An associative array of HTML attributes to apply to the + * anchor tag. If element 'class' is included, it must be an array; 'title' + * 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 or not HTML should be allowed as the link text. If FALSE, + * the text will be run through + * \Drupal\Component\Utility\String::checkPlain() before being output. + * + * @see \Drupal\Core\Routing\UrlGenerator::generateFromPath() + * @see \Drupal\Core\Routing\UrlGenerator::generateFromRoute() + */ +function hook_link_alter(&$variables) { + // Add a warning to the end of route links to the admin section. + if (isset($variables['route_name']) && strpos($variables['route_name'], 'admin') !== FALSE) { + $variables['text'] .= ' (Warning!)'; + } +} + /** * @} End of "addtogroup hooks". */ diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Controller/ViewsUIController.php b/core/modules/views_ui/lib/Drupal/views_ui/Controller/ViewsUIController.php index a78c75741fdd..987148adb265 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/Controller/ViewsUIController.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/Controller/ViewsUIController.php @@ -20,7 +20,8 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Utility\LinkGeneratorInterface; /** * Returns responses for Views UI routes. @@ -44,10 +45,17 @@ class ViewsUIController implements ControllerInterface { /** * The URL generator to use. * - * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface + * @var \Drupal\Core\Routing\UrlGeneratorInterface */ protected $urlGenerator; + /** + * The link generator to use. + * + * @var \Drupal\Core\Utility\LinkGeneratorInterface + */ + protected $linkGenerator; + /** * Constructs a new \Drupal\views_ui\Controller\ViewsUIController object. * @@ -55,13 +63,14 @@ class ViewsUIController implements ControllerInterface { * The Entity manager. * @param \Drupal\views\ViewsData views_data * The Views data cache object. - * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface + * @param \Drupal\Core\Routing\UrlGeneratorInterface * The URL generator. */ - public function __construct(EntityManager $entity_manager, ViewsData $views_data, UrlGeneratorInterface $url_generator) { + public function __construct(EntityManager $entity_manager, ViewsData $views_data, UrlGeneratorInterface $url_generator, LinkGeneratorInterface $link_generator) { $this->entityManager = $entity_manager; $this->viewsData = $views_data; $this->urlGenerator = $url_generator; + $this->linkGenerator = $link_generator; } /** @@ -71,7 +80,8 @@ public static function create(ContainerInterface $container) { return new static( $container->get('plugin.manager.entity'), $container->get('views.views_data'), - $container->get('url_generator') + $container->get('url_generator'), + $container->get('link_generator') ); } @@ -114,7 +124,7 @@ public function reportFields() { foreach ($fields as $field_name => $views) { $rows[$field_name]['data'][0] = check_plain($field_name); foreach ($views as $view) { - $rows[$field_name]['data'][1][] = l($view, "admin/structure/views/view/$view"); + $rows[$field_name]['data'][1][] = $this->linkGenerator->generate($view, 'views_ui.edit', array('view' => $view)); } $rows[$field_name]['data'][1] = implode(', ', $rows[$field_name]['data'][1]); } @@ -142,7 +152,7 @@ public function reportPlugins() { foreach ($rows as &$row) { // Link each view name to the view itself. foreach ($row['views'] as $row_name => $view) { - $row['views'][$row_name] = l($view, "admin/structure/views/view/$view"); + $row['views'][$row_name] = $this->linkGenerator->generate($view, 'views_ui.edit', array('view' => $view)); } $row['views'] = implode(', ', $row['views']); } diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php new file mode 100644 index 000000000000..eb68524fdf87 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php @@ -0,0 +1,407 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Utility\LinkGeneratorTest. + */ + +namespace Drupal\Tests\Core\Utility { + + use Drupal\Core\Language\Language; + use Drupal\Core\Utility\LinkGenerator; + use Drupal\Tests\UnitTestCase; + use Symfony\Cmf\Component\Routing\RouteObjectInterface; + use Symfony\Component\HttpFoundation\Request; + +/** + * Tests the link generator. + * + * @see \Drupal\Core\Utility\LinkGenerator + */ +class LinkGeneratorTest extends UnitTestCase { + + /** + * The tested link generator. + * + * @var \Drupal\Core\Utility\LinkGenerator + */ + protected $linkGenerator; + + /** + * The mocked url generator. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $urlGenerator; + + /** + * The mocked module handler. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; + + /** + * + * The mocked language manager. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $languageManager; + + /** + * Contains the LinkGenerator default options. + */ + protected $defaultOptions = array( + 'query' => array(), + 'html' => FALSE, + 'language' => NULL, + ); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Link generator', + 'description' => 'Tests the link generator.', + 'group' => 'Common', + ); + } + + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGenerator', array(), array(), '', FALSE); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManager'); + + $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->languageManager); + } + + /** + * Setup a proper language manager. + */ + public function setUpLanguageManager() { + $this->languageManager->expects($this->any()) + ->method('getLanguage') + ->will($this->returnValue(new Language(array('id' => 'en')))); + } + + /** + * Provides test data for testing the link method. + * + * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::testGenerateHrefs() + * + * @return array + * Returns some test data. + */ + public function providerTestGenerateHrefs() { + return array( + // Test that the url returned by the URL generator is used. + array('test_route_1', array(), FALSE, '/test-route-1'), + // Test that $parameters is passed to the URL generator. + array('test_route_2', array('value' => 'example'), FALSE, '/test-route-2/example'), + // Test that the 'absolute' option is passed to the URL generator. + array('test_route_3', array(), TRUE, 'http://example.com/test-route-3'), + ); + } + + /** + * Tests the link method with certain hrefs. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::providerTestGenerate() + * + * @dataProvider providerTestGenerateHrefs + */ + public function testGenerateHrefs($route_name, array $parameters, $absolute, $url) { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with($route_name, $parameters, array('absolute' => $absolute) + $this->defaultOptions) + ->will($this->returnValue($url)); + + $this->moduleHandler->expects($this->once()) + ->method('alter'); + + $this->setUpLanguageManager(); + $request = new Request(); + $this->linkGenerator->setRequest($request); + + $result = $this->linkGenerator->generate('Test', $route_name, $parameters, array('absolute' => $absolute)); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array('href' => $url), + ), $result); + } + + /** + * Tests the link method with additional attributes. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateAttributes() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route_1', array(), $this->defaultOptions) + ->will($this->returnValue( + '/test-route-1' + )); + + // Test that HTML attributes are added to the anchor. + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array( + 'attributes' => array('title' => 'Tooltip'), + )); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array( + 'href' => '/test-route-1', + 'title' => 'Tooltip', + ), + ), $result); + } + + /** + * Tests the link method with passed query options. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateQuery() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route_1', array(), array('query' => array('test' => 'value')) + $this->defaultOptions) + ->will($this->returnValue( + '/test-route-1?test=value' + )); + + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array( + 'query' => array('test' => 'value'), + )); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array( + 'href' => '/test-route-1?test=value', + ), + ), $result); + } + + /** + * Tests the link method with passed query options via parameters. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateParametersAsQuery() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route_1', array('test' => 'value'), $this->defaultOptions) + ->will($this->returnValue( + '/test-route-1?test=value' + )); + + $result = $this->linkGenerator->generate('Test', 'test_route_1', array('test' => 'value'), array()); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array( + 'href' => '/test-route-1?test=value', + ), + ), $result); + } + + /** + * Tests the link method with arbitrary passed options. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateOptions() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route_1', array(), array('key' => 'value') + $this->defaultOptions) + ->will($this->returnValue( + '/test-route-1?test=value' + )); + + $result = $this->linkGenerator->generate('Test', 'test_route_1', array(), array( + 'key' => 'value', + )); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array( + 'href' => '/test-route-1?test=value', + ), + ), $result); + } + + /** + * Tests the link method with a script tab. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateXss() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route_4', array(), $this->defaultOptions) + ->will($this->returnValue( + '/test-route-4' + )); + + // Test that HTML link text is escaped by default. + $result = $this->linkGenerator->generate("<script>alert('XSS!')</script>", 'test_route_4'); + $this->assertNotTag(array( + 'tag' => 'a', + 'attributes' => array('href' => '/test-route-4'), + 'child' => array( + 'tag' => 'script', + ), + ), $result); + } + + /** + * Tests the link method with html. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + */ + public function testGenerateWithHtml() { + $this->urlGenerator->expects($this->at(0)) + ->method('generateFromRoute') + ->with('test_route_5', array(), $this->defaultOptions) + ->will($this->returnValue( + '/test-route-5' + )); + $this->urlGenerator->expects($this->at(1)) + ->method('generateFromRoute') + ->with('test_route_5', array(), array('html' => TRUE) + $this->defaultOptions) + ->will($this->returnValue( + '/test-route-5' + )); + + // Test that HTML tags are stripped from the 'title' attribute. + $result = $this->linkGenerator->generate('Test', 'test_route_5', array(), array( + 'attributes' => array('title' => '<em>HTML Tooltip</em>'), + )); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array( + 'href' => '/test-route-5', + 'title' => 'HTML Tooltip', + ), + ), $result); + + // Test that the 'html' option allows unsanitized HTML link text. + $result = $this->linkGenerator->generate('<em>HTML output</em>', 'test_route_5', array(), array('html' => TRUE)); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array('href' => '/test-route-5'), + 'child' => array( + 'tag' => 'em', + ), + ), $result); + } + + /** + * Tests the active class on the link method. + * + * @see \Drupal\Core\Utility\LinkGenerator::generate() + * + * @todo Test that the active class is added on the front page when generating + * links to the front page when drupal_is_front_page() is converted to a + * service. + */ + public function testGenerateActive() { + $this->urlGenerator->expects($this->exactly(6)) + ->method('generateFromRoute') + ->will($this->returnValueMap(array( + array('test_route_1', array(), FALSE, '/test-route-1'), + array('test_route_1', array(), FALSE, '/test-route-1'), + array('test_route_1', array(), FALSE, '/test-route-1'), + array('test_route_1', array(), FALSE, '/test-route-1'), + array('test_route_3', array(), FALSE, '/test-route-3'), + array('test_route_3', array(), FALSE, '/test-route-3'), + ))); + + $this->moduleHandler->expects($this->exactly(6)) + ->method('alter'); + + $this->setUpLanguageManager(); + + // Render a link with a path different from the current path. + $request = new Request(array(), array(), array('system_path' => 'test-route-2')); + $this->linkGenerator->setRequest($request); + $result = $this->linkGenerator->generate('Test', 'test_route_1'); + $this->assertNotTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + + // Render a link with the same path as the current path. + $request = new Request(array(), array(), array('system_path' => 'test-route-1', RouteObjectInterface::ROUTE_NAME => 'test_route_1')); + $this->linkGenerator->setRequest($request); + $result = $this->linkGenerator->generate('Test', 'test_route_1'); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + + // Render a link with the same path and language as the current path. + $result = $this->linkGenerator->generate('Test', 'test_route_1'); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + + // Render a link with the same path but a different language than the current + // path. + $result = $this->linkGenerator->generate( + 'Test', + 'test_route_1', + array(), + array('language' => new Language(array('id' => 'de'))) + ); + $this->assertNotTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + + // Render a link with the same path and query parameter as the current path. + $request = new Request(array('value' => 'example_1'), array(), array('system_path' => 'test-route-3', RouteObjectInterface::ROUTE_NAME => 'test_route_3')); + $parameters = $request->query->all(); + $this->linkGenerator->setRequest($request); + $result = $this->linkGenerator->generate( + 'Test', + 'test_route_3', + array(), + array('query' => array('value' => 'example_1') + )); + $this->assertTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + + // Render a link with the same path but a different query parameter than the + // current path. + $result = $this->linkGenerator->generate( + 'Test', + 'test_route_3', + array(), + array('query' => array('value' => 'example_2')) + ); + $this->assertNotTag(array( + 'tag' => 'a', + 'attributes' => array('class' => 'active'), + ), $result); + } + +} + +} +namespace { + // @todo Remove this once there is a service for drupal_is_front_page(). + if (!function_exists('drupal_is_front_page')) { + function drupal_is_front_page() { + return FALSE; + } + } +} -- GitLab