PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/core/modules/system/src/Tests/Common/UrlTest.php

http://github.com/drupal/drupal
PHP | 320 lines | 202 code | 45 blank | 73 comment | 2 complexity | 9700114d698362f33dd2c2b1ce94af00 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\system\Tests\Common;
  3. use Drupal\Component\Utility\UrlHelper;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\Language\Language;
  6. use Drupal\Core\Render\RenderContext;
  7. use Drupal\Core\Url;
  8. use Drupal\simpletest\WebTestBase;
  9. /**
  10. * Confirm that \Drupal\Core\Url,
  11. * \Drupal\Component\Utility\UrlHelper::filterQueryParameters(),
  12. * \Drupal\Component\Utility\UrlHelper::buildQuery(), and
  13. * \Drupal\Core\Utility\LinkGeneratorInterface::generate()
  14. * work correctly with various input.
  15. *
  16. * @group Common
  17. */
  18. class UrlTest extends WebTestBase {
  19. public static $modules = array('common_test', 'url_alter_test');
  20. /**
  21. * Confirms that invalid URLs are filtered in link generating functions.
  22. */
  23. function testLinkXSS() {
  24. // Test \Drupal::l().
  25. $text = $this->randomMachineName();
  26. $path = "<SCRIPT>alert('XSS')</SCRIPT>";
  27. $encoded_path = "3CSCRIPT%3Ealert%28%27XSS%27%29%3C/SCRIPT%3E";
  28. $link = \Drupal::l($text, Url::fromUserInput('/' . $path));
  29. $this->assertTrue(strpos($link, $encoded_path) !== FALSE && strpos($link, $path) === FALSE, format_string('XSS attack @path was filtered by \Drupal\Core\Utility\LinkGeneratorInterface::generate().', array('@path' => $path)));
  30. // Test \Drupal\Core\Url.
  31. $link = Url::fromUri('base:' . $path)->toString();
  32. $this->assertTrue(strpos($link, $encoded_path) !== FALSE && strpos($link, $path) === FALSE, format_string('XSS attack @path was filtered by #theme', ['@path' => $path]));
  33. }
  34. /**
  35. * Tests that #type=link bubbles outbound route/path processors' metadata.
  36. */
  37. function testLinkBubbleableMetadata() {
  38. $cases = [
  39. ['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT], []],
  40. ['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => Cache::PERMANENT], []],
  41. ['Route processor link', 'route:system.run_cron', [], ['contexts' => ['session'], 'tags' => [], 'max-age' => Cache::PERMANENT], ['placeholders' => []]],
  42. ['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.site', 'session'], 'tags' => [], 'max-age' => Cache::PERMANENT], ['placeholders' => []]],
  43. ['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT], []],
  44. ['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT], []],
  45. ];
  46. foreach ($cases as $case) {
  47. list($title, $uri, $options, $expected_cacheability, $expected_attachments) = $case;
  48. $expected_cacheability['contexts'] = Cache::mergeContexts($expected_cacheability['contexts'], ['languages:language_interface', 'theme', 'user.permissions']);
  49. $link = [
  50. '#type' => 'link',
  51. '#title' => $title,
  52. '#options' => $options,
  53. '#url' => Url::fromUri($uri),
  54. ];
  55. \Drupal::service('renderer')->renderRoot($link);
  56. $this->pass($title);
  57. $this->assertEqual($expected_cacheability, $link['#cache']);
  58. $this->assertEqual($expected_attachments, $link['#attached']);
  59. }
  60. }
  61. /**
  62. * Tests that default and custom attributes are handled correctly on links.
  63. */
  64. function testLinkAttributes() {
  65. /** @var \Drupal\Core\Render\RendererInterface $renderer */
  66. $renderer = $this->container->get('renderer');
  67. // Test that hreflang is added when a link has a known language.
  68. $language = new Language(array('id' => 'fr', 'name' => 'French'));
  69. $hreflang_link = array(
  70. '#type' => 'link',
  71. '#options' => array(
  72. 'language' => $language,
  73. ),
  74. '#url' => Url::fromUri('https://www.drupal.org'),
  75. '#title' => 'bar',
  76. );
  77. $langcode = $language->getId();
  78. // Test that the default hreflang handling for links does not override a
  79. // hreflang attribute explicitly set in the render array.
  80. $hreflang_override_link = $hreflang_link;
  81. $hreflang_override_link['#options']['attributes']['hreflang'] = 'foo';
  82. $rendered = $renderer->renderRoot($hreflang_link);
  83. $this->assertTrue($this->hasAttribute('hreflang', $rendered, $langcode), format_string('hreflang attribute with value @langcode is present on a rendered link when langcode is provided in the render array.', array('@langcode' => $langcode)));
  84. $rendered = $renderer->renderRoot($hreflang_override_link);
  85. $this->assertTrue($this->hasAttribute('hreflang', $rendered, 'foo'), format_string('hreflang attribute with value @hreflang is present on a rendered link when @hreflang is provided in the render array.', array('@hreflang' => 'foo')));
  86. // Test the active class in links produced by
  87. // \Drupal\Core\Utility\LinkGeneratorInterface::generate() and #type 'link'.
  88. $options_no_query = array();
  89. $options_query = array(
  90. 'query' => array(
  91. 'foo' => 'bar',
  92. 'one' => 'two',
  93. ),
  94. );
  95. $options_query_reverse = array(
  96. 'query' => array(
  97. 'one' => 'two',
  98. 'foo' => 'bar',
  99. ),
  100. );
  101. // Test #type link.
  102. $path = 'common-test/type-link-active-class';
  103. $this->drupalGet($path, $options_no_query);
  104. $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active'));
  105. $this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page is marked active.');
  106. $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active'));
  107. $this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page with a query string when the current page has no query string is not marked active.');
  108. $this->drupalGet($path, $options_query);
  109. $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active'));
  110. $this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page with a query string that matches the current query string is marked active.');
  111. $links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query_reverse)->toString(), ':class' => 'is-active'));
  112. $this->assertTrue(isset($links[0]), 'A link generated by the link generator 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.');
  113. $links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active'));
  114. $this->assertTrue(isset($links[0]), 'A link generated by the link generator to the current page without a query string when the current page has a query string is not marked active.');
  115. // Test adding a custom class in links produced by
  116. // \Drupal\Core\Utility\LinkGeneratorInterface::generate() and #type 'link'.
  117. // Test the link generator.
  118. $class_l = $this->randomMachineName();
  119. $link_l = \Drupal::l($this->randomMachineName(), new Url('<current>', [], ['attributes' => ['class' => [$class_l]]]));
  120. $this->assertTrue($this->hasAttribute('class', $link_l, $class_l), format_string('Custom class @class is present on link when requested by l()', array('@class' => $class_l)));
  121. // Test #type.
  122. $class_theme = $this->randomMachineName();
  123. $type_link = array(
  124. '#type' => 'link',
  125. '#title' => $this->randomMachineName(),
  126. '#url' => Url::fromRoute('<current>'),
  127. '#options' => array(
  128. 'attributes' => array(
  129. 'class' => array($class_theme),
  130. ),
  131. ),
  132. );
  133. $link_theme = $renderer->renderRoot($type_link);
  134. $this->assertTrue($this->hasAttribute('class', $link_theme, $class_theme), format_string('Custom class @class is present on link when requested by #type', array('@class' => $class_theme)));
  135. }
  136. /**
  137. * Tests that link functions support render arrays as 'text'.
  138. */
  139. function testLinkRenderArrayText() {
  140. /** @var \Drupal\Core\Render\RendererInterface $renderer */
  141. $renderer = $this->container->get('renderer');
  142. // Build a link with the link generator for reference.
  143. $l = \Drupal::l('foo', Url::fromUri('https://www.drupal.org'));
  144. // Test a renderable array passed to the link generator.
  145. $renderer->executeInRenderContext(new RenderContext(), function() use ($renderer, $l) {
  146. $renderable_text = array('#markup' => 'foo');
  147. $l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org'));
  148. $this->assertEqual($l_renderable_text, $l);
  149. });
  150. // Test a themed link with plain text 'text'.
  151. $type_link_plain_array = array(
  152. '#type' => 'link',
  153. '#title' => 'foo',
  154. '#url' => Url::fromUri('https://www.drupal.org'),
  155. );
  156. $type_link_plain = $renderer->renderRoot($type_link_plain_array);
  157. $this->assertEqual($type_link_plain, $l);
  158. // Build a themed link with renderable 'text'.
  159. $type_link_nested_array = array(
  160. '#type' => 'link',
  161. '#title' => array('#markup' => 'foo'),
  162. '#url' => Url::fromUri('https://www.drupal.org'),
  163. );
  164. $type_link_nested = $renderer->renderRoot($type_link_nested_array);
  165. $this->assertEqual($type_link_nested, $l);
  166. }
  167. /**
  168. * Checks for class existence in link.
  169. *
  170. * @param $link
  171. * URL to search.
  172. * @param $class
  173. * Element class to search for.
  174. *
  175. * @return bool
  176. * TRUE if the class is found, FALSE otherwise.
  177. */
  178. private function hasAttribute($attribute, $link, $class) {
  179. return preg_match('|' . $attribute . '="([^\"\s]+\s+)*' . $class . '|', $link);
  180. }
  181. /**
  182. * Tests UrlHelper::filterQueryParameters().
  183. */
  184. function testDrupalGetQueryParameters() {
  185. $original = array(
  186. 'a' => 1,
  187. 'b' => array(
  188. 'd' => 4,
  189. 'e' => array(
  190. 'f' => 5,
  191. ),
  192. ),
  193. 'c' => 3,
  194. );
  195. // First-level exclusion.
  196. $result = $original;
  197. unset($result['b']);
  198. $this->assertEqual(UrlHelper::filterQueryParameters($original, array('b')), $result, "'b' was removed.");
  199. // Second-level exclusion.
  200. $result = $original;
  201. unset($result['b']['d']);
  202. $this->assertEqual(UrlHelper::filterQueryParameters($original, array('b[d]')), $result, "'b[d]' was removed.");
  203. // Third-level exclusion.
  204. $result = $original;
  205. unset($result['b']['e']['f']);
  206. $this->assertEqual(UrlHelper::filterQueryParameters($original, array('b[e][f]')), $result, "'b[e][f]' was removed.");
  207. // Multiple exclusions.
  208. $result = $original;
  209. unset($result['a'], $result['b']['e'], $result['c']);
  210. $this->assertEqual(UrlHelper::filterQueryParameters($original, array('a', 'b[e]', 'c')), $result, "'a', 'b[e]', 'c' were removed.");
  211. }
  212. /**
  213. * Tests UrlHelper::parse().
  214. */
  215. function testDrupalParseUrl() {
  216. // Relative, absolute, and external URLs, without/with explicit script path,
  217. // without/with Drupal path.
  218. foreach (array('', '/', 'https://www.drupal.org/') as $absolute) {
  219. foreach (array('', 'index.php/') as $script) {
  220. foreach (array('', 'foo/bar') as $path) {
  221. $url = $absolute . $script . $path . '?foo=bar&bar=baz&baz#foo';
  222. $expected = array(
  223. 'path' => $absolute . $script . $path,
  224. 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''),
  225. 'fragment' => 'foo',
  226. );
  227. $this->assertEqual(UrlHelper::parse($url), $expected, 'URL parsed correctly.');
  228. }
  229. }
  230. }
  231. // Relative URL that is known to confuse parse_url().
  232. $url = 'foo/bar:1';
  233. $result = array(
  234. 'path' => 'foo/bar:1',
  235. 'query' => array(),
  236. 'fragment' => '',
  237. );
  238. $this->assertEqual(UrlHelper::parse($url), $result, 'Relative URL parsed correctly.');
  239. // Test that drupal can recognize an absolute URL. Used to prevent attack vectors.
  240. $url = 'https://www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo';
  241. $this->assertTrue(UrlHelper::isExternal($url), 'Correctly identified an external URL.');
  242. // Test that UrlHelper::parse() does not allow spoofing a URL to force a malicious redirect.
  243. $parts = UrlHelper::parse('forged:http://cwe.mitre.org/data/definitions/601.html');
  244. $this->assertFalse(UrlHelper::isValid($parts['path'], TRUE), '\Drupal\Component\Utility\UrlHelper::isValid() correctly parsed a forged URL.');
  245. }
  246. /**
  247. * Tests external URL handling.
  248. */
  249. function testExternalUrls() {
  250. $test_url = 'https://www.drupal.org/';
  251. // Verify external URL can contain a fragment.
  252. $url = $test_url . '#drupal';
  253. $result = Url::fromUri($url)->toString();
  254. $this->assertEqual($url, $result, 'External URL with fragment works without a fragment in $options.');
  255. // Verify fragment can be overridden in an external URL.
  256. $url = $test_url . '#drupal';
  257. $fragment = $this->randomMachineName(10);
  258. $result = Url::fromUri($url, array('fragment' => $fragment))->toString();
  259. $this->assertEqual($test_url . '#' . $fragment, $result, 'External URL fragment is overridden with a custom fragment in $options.');
  260. // Verify external URL can contain a query string.
  261. $url = $test_url . '?drupal=awesome';
  262. $result = Url::fromUri($url)->toString();
  263. $this->assertEqual($url, $result);
  264. // Verify external URL can be extended with a query string.
  265. $url = $test_url;
  266. $query = array($this->randomMachineName(5) => $this->randomMachineName(5));
  267. $result = Url::fromUri($url, array('query' => $query))->toString();
  268. $this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, 'External URL can be extended with a query string in $options.');
  269. // Verify query string can be extended in an external URL.
  270. $url = $test_url . '?drupal=awesome';
  271. $query = array($this->randomMachineName(5) => $this->randomMachineName(5));
  272. $result = Url::fromUri($url, array('query' => $query))->toString();
  273. $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result);
  274. }
  275. }