diff --git a/core/includes/common.inc b/core/includes/common.inc index 86fc44aae9c734fbfabb18c955c0e48daaeee08c..7a45354f9d9867d15b2f3add6210ef3d5988eecd 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2161,6 +2161,46 @@ function url_is_external($path) { } /** + * Returns TRUE if the given path is currently being viewed. + * + * @param string $path + * An internal path or external url as could be used by url(). + * + * @param array $options + * An associative array of additional options. Defaults to an empty array. It + * may contain the following elements. + * - '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). This element is also used by url(). + * - 'query': An array of query key/value-pairs (without any URL-encoding) to + * append to the URL. + * + * @return bool + * Boolean where TRUE indicates an active path. + * + * @see url() + */ +function url_is_active($path, $options = array()) { + // Merge in defaults. + $options += array( + 'query' => array(), + 'language' => NULL, + ); + + // A path is only active if it corresponds to the current path, the + // language of the path is equal to the current language, and if the + // query parameters of the url equal those of the current request, since the + // same request with different query parameters may yield a different page + // (e.g., pagers). + $path_match = ($path == current_path() || ($path == '' && drupal_is_front_page())); + $lang_match = (empty($options['language']) || $options['language']->langcode == language(LANGUAGE_TYPE_URL)->langcode); + $query_match = drupal_container()->get('request')->query->all() == $options['query']; + + return $path_match && $lang_match && $query_match; +} + +/** * Formats an attribute string for an HTTP header. * * @param $attributes @@ -2229,68 +2269,50 @@ function drupal_http_header_attributes(array $attributes = array()) { * An HTML string containing a link to the given path. * * @see url() + * @see theme_link() */ function l($text, $path, array $options = array()) { - static $use_theme = NULL; + // Build a variables array to keep the structure of the alter consistent with + // theme_link(). + $variables = array( + 'text' => $text, + 'path' => $path, + 'options' => $options, + ); // Merge in defaults. - $options += array( + $variables['options'] += array( 'attributes' => array(), 'query' => array(), 'html' => FALSE, ); - // Append active class. - // The link is only active, if its path corresponds to the current path, the - // language of the linked path is equal to the current language, and if the - // query parameters of the link equal those of the current request, since the - // same request with different query parameters may yield a different page - // (e.g., pagers). - $is_active = ($path == current_path() || ($path == '' && drupal_is_front_page())); - $is_active = $is_active && (empty($options['language']) || $options['language']->langcode == language(LANGUAGE_TYPE_URL)->langcode); - $is_active = $is_active && (drupal_container()->get('request')->query->all() == $options['query']); - if ($is_active) { - $options['attributes']['class'][] = 'active'; - } - - // Remove all HTML and PHP tags from a tooltip. For best performance, we act only - // if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive). - if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) { - $options['attributes']['title'] = strip_tags($options['attributes']['title']); - } - - // Determine if rendering of the link is to be done with a theme function - // or the inline default. Inline is faster, but if the theme system has been - // loaded and a module or theme implements a preprocess or process function - // or overrides the theme_link() function, then invoke theme(). Preliminary - // benchmarks indicate that invoking theme() can slow down the l() function - // by 20% or more, and that some of the link-heavy Drupal pages spend more - // than 10% of the total page request time in the l() function. - if (!isset($use_theme) && function_exists('theme')) { - // Allow edge cases to prevent theme initialization and force inline link - // rendering. - if (config('system.performance')->get('theme_link')) { - drupal_theme_initialize(); - $registry = theme_get_registry(FALSE); - // We don't want to duplicate functionality that's in theme(), so any - // hint of a module or theme doing anything at all special with the 'link' - // theme hook should simply result in theme() being called. This includes - // the overriding of theme_link() with an alternate function or template, - // the presence of preprocess or process functions, or the presence of - // include files. - $use_theme = !isset($registry['link']['function']) || ($registry['link']['function'] != 'theme_link'); - $use_theme = $use_theme || !empty($registry['link']['preprocess functions']) || !empty($registry['link']['process functions']) || !empty($registry['link']['includes']); - } - else { - $use_theme = FALSE; - } + // Add 'active' class if appropriate. + if ($variables['url_is_active'] = url_is_active($variables['path'], $variables['options'])) { + $variables['options']['attributes']['class'][] = 'active'; } - if ($use_theme) { - return theme('link', array('text' => $text, 'path' => $path, 'options' => $options)); + + // 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. + drupal_container()->get('module_handler')->alter('link', $variables); + + // Move attributes out of options. url() doesn't need them. + $attributes = new Attribute($variables['options']['attributes']); + unset($variables['options']['attributes']); + // The result of url() is a plain-text URL. Because we are using it here // in an HTML argument context, we need to encode it properly. - return '' . ($options['html'] ? $text : check_plain($text)) . ''; + $url = check_plain(url($variables['path'], $variables['options'])); + + // Sanitize the link text if necessary. + $text = ($variables['options']['html']) ? $variables['text'] : check_plain($variables['text']); + + return '' . $text . ''; } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 8d527df2aae1606a01b7db85685a511306f68712..b5a94edb4ae8e432a87fcf9633475b5ff7ccb737 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1683,14 +1683,12 @@ function theme_status_messages($variables) { /** * Returns HTML for a link. * - * All Drupal code that outputs a link should call the l() function. That - * function performs some initial preprocessing, and then, if necessary, calls - * theme('link') for rendering the anchor tag. + * This is a wrapper around l() to allow for more advanced themeing and use of + * array(#theme => 'link') style render arrays. * - * To optimize performance for sites that don't need custom theming of links, - * the l() function includes an inline copy of this function, and uses that - * copy if none of the enabled modules or the active theme implement any - * preprocess or process functions or override this theme implementation. + * Where performance is more important than theme flexibility, always call l() + * directly. There is a measurable performance impact when using theme('link') + * style implementations on link-heavy sites. * * @param $variables * An associative array containing the keys 'text', 'path', and 'options'. @@ -1698,8 +1696,8 @@ function theme_status_messages($variables) { * * @see l() */ -function theme_link($variables) { - return '' . ($variables['options']['html'] ? $variables['text'] : check_plain($variables['text'])) . ''; +function theme_link(&$variables) { + return l($variables['text'], $variables['path'], $variables['options']); } /** diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php index ac834d1167efb362fddc0272441f8668b05742ec..3a108af668738dab2c191494a3dddb816fbac18b 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageSwitchingTest.php @@ -19,7 +19,7 @@ class LanguageSwitchingTest extends WebTestBase { * * @var array */ - public static $modules = array('language', 'block'); + public static $modules = array('language', 'block', 'language_test'); public static function getInfo() { return array( @@ -89,4 +89,101 @@ function testLanguageBlock() { $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language anchor is marked as active on the language switcher block.'); } + /** + * Test active class on links when switching language. + */ + function testLinkLanguageSwitchingActiveClass() { + // Add language. + $edit = array( + 'predefined_langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Enable URL language detection and selection. + $edit = array('language_interface[enabled][language-url]' => '1'); + $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings')); + + // Test l(). + $function_name = 'l()'; + + // Test URLs on an English page. + $current_language = 'English'; + $this->drupalGet('language_test/l-active-class'); + + // No lang link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // en link should be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'en_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // fr link should NOT be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'fr_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Test URLs on a French page. + $current_language = 'French'; + $this->drupalGet('fr/language_test/l-active-class'); + + // No lang link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // en link should NOT be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'en_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // fr link should be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'fr_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Test theme('link'). + $function_name = "theme('link')"; + + // Test URLs on an English page. + $current_language = 'English'; + $this->drupalGet('language_test/theme-link-active-class'); + + // No lang link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // en link should be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'en_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // fr link should NOT be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'fr_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // Test URLs on a French page. + $current_language = 'French'; + $this->drupalGet('fr/language_test/theme-link-active-class'); + + // No lang link should be active. + $langcode = 'none'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'no_lang_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // en link should NOT be active. + $langcode = 'en'; + $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', array(':id' => 'en_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + + // fr link should be active. + $langcode = 'fr'; + $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', array(':id' => 'fr_link', ':class' => 'active')); + $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', array(':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode))); + } + } diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module index cb97937ebb2bbcfed99a8f15f2a0170c051c51a3..43457dfd7d9f0b4228fb92847d5fe2102dad5c1e 100644 --- a/core/modules/language/tests/language_test/language_test.module +++ b/core/modules/language/tests/language_test/language_test.module @@ -109,6 +109,16 @@ function language_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['language_test/l-active-class'] = array( + 'page callback' => 'language_test_l_active_class', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['language_test/theme-link-active-class'] = array( + 'page callback' => 'language_test_theme_link_active_class', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -119,3 +129,89 @@ function language_test_menu() { function language_test_subrequest() { return drupal_container()->get('http_kernel')->handle(Request::create('/user'), HttpKernelInterface::SUB_REQUEST); } + +/** + * Page callback: Displays links to the current page, with different langcodes. + * + * Using #theme causes these links to be rendered with theme_link(). + */ +function language_test_theme_link_active_class() { + $languages = language_list(); + return array( + 'no_language' => array( + '#theme' => 'link', + '#text' => t('Link to the current path with no langcode provided.'), + '#path' => current_path(), + '#options' => array( + 'attributes' => array( + 'id' => array('no_lang_link'), + ), + ), + ), + 'fr' => array( + '#theme' => 'link', + '#text' => t('Link to a French version of the current path.'), + '#path' => current_path(), + '#options' => array( + 'language' => $languages['fr'], + 'attributes' => array( + 'id' => array('fr_link'), + ), + ), + ), + 'en' => array( + '#theme' => 'link', + '#text' => t('Link to an English version of the current path.'), + '#path' => current_path(), + '#options' => array( + 'language' => $languages['en'], + 'attributes' => array( + 'id' => array('en_link'), + ), + ), + ), + ); +} + +/** + * Page callback: Displays links to the current page, with different langcodes. + * + * Using #type causes these links to be rendered with l(). + */ +function language_test_l_active_class() { + $languages = language_list(); + return array( + 'no_language' => array( + '#type' => 'link', + '#title' => t('Link to the current path with no langcode provided.'), + '#href' => current_path(), + '#options' => array( + 'attributes' => array( + 'id' => array('no_lang_link'), + ), + ), + ), + 'fr' => array( + '#type' => 'link', + '#title' => t('Link to a French version of the current path.'), + '#href' => current_path(), + '#options' => array( + 'language' => $languages['fr'], + 'attributes' => array( + 'id' => array('fr_link'), + ), + ), + ), + 'en' => array( + '#type' => 'link', + '#title' => t('Link to an English version of the current path.'), + '#href' => current_path(), + '#options' => array( + 'language' => $languages['en'], + 'attributes' => array( + 'id' => array('en_link'), + ), + ), + ), + ); +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php index eeb49ccd2f2d78ed9d62b309981cfff1485f2a93..1a3fa14b5e4fad4ab23999632f1d983de8f23863 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php @@ -28,47 +28,99 @@ public static function getInfo() { } /** - * Confirms that invalid URLs are filtered. + * Confirms that invalid URLs are filtered in link generating functions. */ - function testLXSS() { + function testLinkXSS() { + // Test l(). $text = $this->randomName(); $path = ""; $link = l($text, $path); $sanitized_path = check_url(url($path)); - $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered', array('@path' => $path))); + $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by l().', array('@path' => $path))); + + // Test #theme. + $link_array = array( + '#theme' => 'link', + '#text' => $this->randomName(), + '#path' => $path, + ); + $theme_link = render($link_array); + $sanitized_path = check_url(url($path)); + $this->assertTrue(strpos($theme_link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by #theme', array('@path' => $path))); } /** - * Tests for active class in l() function. + * Tests for active class in l() and '#theme' => 'link' rendarable arrays. */ - function testLActiveClass() { + function testLinkActiveClass() { + $options_no_query = array(); + $options_query = array('query' => array('foo' => 'bar', 'one' => 'two',)); + $options_query_reverse = array('query' => array('one' => 'two', 'foo' => 'bar',)); + + // Test l(). $path = 'common-test/l-active-class'; - $options = array(); - $this->drupalGet($path, $options); - $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options), ':class' => 'active')); - $this->assertTrue(isset($links[0]), 'A link to the current page is marked active.'); + $this->drupalGet($path, $options_no_query); + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_no_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page is marked active.'); + + $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string when the current page has no query string is not marked active.'); + + $this->drupalGet($path, $options_query); + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string that matches the current query string is marked active.'); + + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query_reverse), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.'); - $options = array('query' => array('foo' => 'bar')); - $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options), ':class' => 'active')); - $this->assertTrue(isset($links[0]), 'A link to the current page with a query string when the current page has no query string is not marked active.'); + $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_no_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by l() to the current page without a query string when the current page has a query string is not marked active.'); - $this->drupalGet($path, $options); - $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options), ':class' => 'active')); - $this->assertTrue(isset($links[0]), 'A link to the current page with a query string that matches the current query string is marked active.'); + // Test #theme. + $path = 'common-test/theme-link-active-class'; - $options = array(); - $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options), ':class' => 'active')); - $this->assertTrue(isset($links[0]), 'A link to the current page without a query string when the current page has a query string is not marked active.'); + $this->drupalGet($path, $options_no_query); + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_no_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page is marked active.'); + + $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string when the current page has no query string is not marked active.'); + + $this->drupalGet($path, $options_query); + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string that matches the current query string is marked active.'); + + $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => url($path, $options_query_reverse), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.'); + + $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => url($path, $options_no_query), ':class' => 'active')); + $this->assertTrue(isset($links[0]), 'A link generated by #theme to the current page without a query string when the current page has a query string is not marked active.'); } /** - * Tests for custom class in l() function. + * Tests for custom class in l() and '#theme' => 'link' renderable arrays. */ - function testLCustomClass() { - $class = $this->randomName(); - $link = l($this->randomName(), current_path(), array('attributes' => array('class' => array($class)))); - $this->assertTrue($this->hasClass($link, $class), format_string('Custom class @class is present on link when requested', array('@class' => $class))); + function testLinkCustomClass() { + // Test l(). + $class_l = $this->randomName(); + $link_l = l($this->randomName(), current_path(), array('attributes' => array('class' => array($class_l)))); + $this->assertTrue($this->hasClass($link_l, $class_l), format_string('Custom class @class is present on link when requested by l()', array('@class' => $class_l))); + + // Test #theme. + $class_theme = $this->randomName(); + $theme_link = array( + '#theme' => 'link', + '#text' => $this->randomName(), + '#path' => current_path(), + '#options' => array( + 'attributes' => array( + 'class' => array($class_theme), + ), + ), + ); + $link_theme = render($theme_link); + $this->assertTrue($this->hasClass($link_theme, $class_theme), format_string('Custom class @class is present on link when requested by #theme', array('@class' => $class_theme))); } /** diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module index f932f0fd542d7ccbf33b50ef5a6829157648a1d4..829bd56e2570cb36cf3da0b3b60d58a4ada13305 100644 --- a/core/modules/system/tests/modules/common_test/common_test.module +++ b/core/modules/system/tests/modules/common_test/common_test.module @@ -70,6 +70,12 @@ function common_test_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['common-test/theme-link-active-class'] = array( + 'title' => 'Test theme_link() adding of active class', + 'page callback' => 'common_test_theme_link_active_class', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -320,6 +326,8 @@ function common_test_cron() { /** * Page callback: Displays links to the current page, one with a query string. + * + * Using #type causes these links to be rendered with l(). */ function common_test_l_active_class() { return array( @@ -335,6 +343,55 @@ function common_test_l_active_class() { '#options' => array( 'query' => array( 'foo' => 'bar', + 'one' => 'two', + ), + ), + ), + 'with_query_reversed' => array( + '#type' => 'link', + '#title' => t('Link with the same query string in reverse order'), + '#href' => current_path(), + '#options' => array( + 'query' => array( + 'one' => 'two', + 'foo' => 'bar', + ), + ), + ), + ); +} + +/** + * Page callback: Displays links to the current page, one with a query string. + * + * Using #theme causes these links to be rendered with theme_link(). + */ +function common_test_theme_link_active_class() { + return array( + 'no_query' => array( + '#theme' => 'link', + '#text' => t('Link with no query string'), + '#path' => current_path(), + ), + 'with_query' => array( + '#theme' => 'link', + '#text' => t('Link with a query string'), + '#path' => current_path(), + '#options' => array( + 'query' => array( + 'foo' => 'bar', + 'one' => 'two', + ), + ), + ), + 'with_query_reversed' => array( + '#theme' => 'link', + '#text' => t('Link with the same query string in reverse order'), + '#path' => current_path(), + '#options' => array( + 'query' => array( + 'one' => 'two', + 'foo' => 'bar', ), ), ),