UrlHelperTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <?php
  2. namespace Drupal\Tests\Component\Utility;
  3. use Drupal\Component\Utility\UrlHelper;
  4. use PHPUnit\Framework\TestCase;
  5. /**
  6. * @group Utility
  7. *
  8. * @coversDefaultClass \Drupal\Component\Utility\UrlHelper
  9. */
  10. class UrlHelperTest extends TestCase {
  11. /**
  12. * Provides test data for testBuildQuery().
  13. *
  14. * @return array
  15. */
  16. public function providerTestBuildQuery() {
  17. return [
  18. [['a' => ' &#//+%20@۞'], 'a=%20%26%23//%2B%2520%40%DB%9E', 'Value was properly encoded.'],
  19. [[' &#//+%20@۞' => 'a'], '%20%26%23%2F%2F%2B%2520%40%DB%9E=a', 'Key was properly encoded.'],
  20. [['a' => '1', 'b' => '2', 'c' => '3'], 'a=1&b=2&c=3', 'Multiple values were properly concatenated.'],
  21. [['a' => ['b' => '2', 'c' => '3'], 'd' => 'foo'], 'a%5Bb%5D=2&a%5Bc%5D=3&d=foo', 'Nested array was properly encoded.'],
  22. [['foo' => NULL], 'foo', 'Simple parameters are properly added.'],
  23. ];
  24. }
  25. /**
  26. * Tests query building.
  27. *
  28. * @dataProvider providerTestBuildQuery
  29. * @covers ::buildQuery
  30. *
  31. * @param array $query
  32. * The array of query parameters.
  33. * @param string $expected
  34. * The expected query string.
  35. * @param string $message
  36. * The assertion message.
  37. */
  38. public function testBuildQuery($query, $expected, $message) {
  39. $this->assertEquals(UrlHelper::buildQuery($query), $expected, $message);
  40. }
  41. /**
  42. * Data provider for testValidAbsolute().
  43. *
  44. * @return array
  45. */
  46. public function providerTestValidAbsoluteData() {
  47. $urls = [
  48. 'example.com',
  49. 'www.example.com',
  50. 'ex-ample.com',
  51. '3xampl3.com',
  52. 'example.com/parenthesis',
  53. 'example.com/index.html#pagetop',
  54. 'example.com:8080',
  55. 'subdomain.example.com',
  56. 'example.com/index.php/node',
  57. 'example.com/index.php/node?param=false',
  58. 'user@www.example.com',
  59. 'user:pass@www.example.com:8080/login.php?do=login&style=%23#pagetop',
  60. '127.0.0.1',
  61. 'example.org?',
  62. 'john%20doe:secret:foo@example.org/',
  63. 'example.org/~,$\'*;',
  64. 'caf%C3%A9.example.org',
  65. '[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
  66. ];
  67. return $this->dataEnhanceWithScheme($urls);
  68. }
  69. /**
  70. * Tests valid absolute URLs.
  71. *
  72. * @dataProvider providerTestValidAbsoluteData
  73. * @covers ::isValid
  74. *
  75. * @param string $url
  76. * The url to test.
  77. * @param string $scheme
  78. * The scheme to test.
  79. */
  80. public function testValidAbsolute($url, $scheme) {
  81. $test_url = $scheme . '://' . $url;
  82. $valid_url = UrlHelper::isValid($test_url, TRUE);
  83. $this->assertTrue($valid_url, $test_url . ' is a valid URL.');
  84. }
  85. /**
  86. * Provides data for testInvalidAbsolute().
  87. *
  88. * @return array
  89. */
  90. public function providerTestInvalidAbsolute() {
  91. $data = [
  92. '',
  93. 'ex!ample.com',
  94. 'ex%ample.com',
  95. ];
  96. return $this->dataEnhanceWithScheme($data);
  97. }
  98. /**
  99. * Tests invalid absolute URLs.
  100. *
  101. * @dataProvider providerTestInvalidAbsolute
  102. * @covers ::isValid
  103. *
  104. * @param string $url
  105. * The url to test.
  106. * @param string $scheme
  107. * The scheme to test.
  108. */
  109. public function testInvalidAbsolute($url, $scheme) {
  110. $test_url = $scheme . '://' . $url;
  111. $valid_url = UrlHelper::isValid($test_url, TRUE);
  112. $this->assertFalse($valid_url, $test_url . ' is NOT a valid URL.');
  113. }
  114. /**
  115. * Provides data for testValidRelative().
  116. *
  117. * @return array
  118. */
  119. public function providerTestValidRelativeData() {
  120. $data = [
  121. 'paren(the)sis',
  122. 'index.html#pagetop',
  123. 'index.php/node',
  124. 'index.php/node?param=false',
  125. 'login.php?do=login&style=%23#pagetop',
  126. ];
  127. return $this->dataEnhanceWithPrefix($data);
  128. }
  129. /**
  130. * Tests valid relative URLs.
  131. *
  132. * @dataProvider providerTestValidRelativeData
  133. * @covers ::isValid
  134. *
  135. * @param string $url
  136. * The url to test.
  137. * @param string $prefix
  138. * The prefix to test.
  139. */
  140. public function testValidRelative($url, $prefix) {
  141. $test_url = $prefix . $url;
  142. $valid_url = UrlHelper::isValid($test_url);
  143. $this->assertTrue($valid_url, $test_url . ' is a valid URL.');
  144. }
  145. /**
  146. * Provides data for testInvalidRelative().
  147. *
  148. * @return array
  149. */
  150. public function providerTestInvalidRelativeData() {
  151. $data = [
  152. 'ex^mple',
  153. 'example<>',
  154. 'ex%ample',
  155. ];
  156. return $this->dataEnhanceWithPrefix($data);
  157. }
  158. /**
  159. * Tests invalid relative URLs.
  160. *
  161. * @dataProvider providerTestInvalidRelativeData
  162. * @covers ::isValid
  163. *
  164. * @param string $url
  165. * The url to test.
  166. * @param string $prefix
  167. * The prefix to test.
  168. */
  169. public function testInvalidRelative($url, $prefix) {
  170. $test_url = $prefix . $url;
  171. $valid_url = UrlHelper::isValid($test_url);
  172. $this->assertFalse($valid_url, $test_url . ' is NOT a valid URL.');
  173. }
  174. /**
  175. * Tests query filtering.
  176. *
  177. * @dataProvider providerTestFilterQueryParameters
  178. * @covers ::filterQueryParameters
  179. *
  180. * @param array $query
  181. * The array of query parameters.
  182. * @param array $exclude
  183. * A list of $query array keys to remove. Use "parent[child]" to exclude
  184. * nested items.
  185. * @param array $expected
  186. * An array containing query parameters.
  187. */
  188. public function testFilterQueryParameters($query, $exclude, $expected) {
  189. $filtered = UrlHelper::filterQueryParameters($query, $exclude);
  190. $this->assertEquals($expected, $filtered, 'The query was not properly filtered.');
  191. }
  192. /**
  193. * Provides data to self::testFilterQueryParameters().
  194. *
  195. * @return array
  196. */
  197. public static function providerTestFilterQueryParameters() {
  198. return [
  199. // Test without an exclude filter.
  200. [
  201. 'query' => ['a' => ['b' => 'c']],
  202. 'exclude' => [],
  203. 'expected' => ['a' => ['b' => 'c']],
  204. ],
  205. // Exclude the 'b' element.
  206. [
  207. 'query' => ['a' => ['b' => 'c', 'd' => 'e']],
  208. 'exclude' => ['a[b]'],
  209. 'expected' => ['a' => ['d' => 'e']],
  210. ],
  211. ];
  212. }
  213. /**
  214. * Tests url parsing.
  215. *
  216. * @dataProvider providerTestParse
  217. * @covers ::parse
  218. *
  219. * @param string $url
  220. * URL to test.
  221. * @param array $expected
  222. * Associative array with expected parameters.
  223. */
  224. public function testParse($url, $expected) {
  225. $parsed = UrlHelper::parse($url);
  226. $this->assertEquals($expected, $parsed, 'The URL was not properly parsed.');
  227. }
  228. /**
  229. * Provides data for self::testParse().
  230. *
  231. * @return array
  232. */
  233. public static function providerTestParse() {
  234. return [
  235. [
  236. 'http://www.example.com/my/path',
  237. [
  238. 'path' => 'http://www.example.com/my/path',
  239. 'query' => [],
  240. 'fragment' => '',
  241. ],
  242. ],
  243. [
  244. 'http://www.example.com/my/path?destination=home#footer',
  245. [
  246. 'path' => 'http://www.example.com/my/path',
  247. 'query' => [
  248. 'destination' => 'home',
  249. ],
  250. 'fragment' => 'footer',
  251. ],
  252. ],
  253. 'absolute fragment, no query' => [
  254. 'http://www.example.com/my/path#footer',
  255. [
  256. 'path' => 'http://www.example.com/my/path',
  257. 'query' => [],
  258. 'fragment' => 'footer',
  259. ],
  260. ],
  261. [
  262. 'http://',
  263. [
  264. 'path' => '',
  265. 'query' => [],
  266. 'fragment' => '',
  267. ],
  268. ],
  269. [
  270. 'https://',
  271. [
  272. 'path' => '',
  273. 'query' => [],
  274. 'fragment' => '',
  275. ],
  276. ],
  277. [
  278. '/my/path?destination=home#footer',
  279. [
  280. 'path' => '/my/path',
  281. 'query' => [
  282. 'destination' => 'home',
  283. ],
  284. 'fragment' => 'footer',
  285. ],
  286. ],
  287. 'relative fragment, no query' => [
  288. '/my/path#footer',
  289. [
  290. 'path' => '/my/path',
  291. 'query' => [],
  292. 'fragment' => 'footer',
  293. ],
  294. ],
  295. ];
  296. }
  297. /**
  298. * Tests path encoding.
  299. *
  300. * @dataProvider providerTestEncodePath
  301. * @covers ::encodePath
  302. *
  303. * @param string $path
  304. * A path to encode.
  305. * @param string $expected
  306. * The expected encoded path.
  307. */
  308. public function testEncodePath($path, $expected) {
  309. $encoded = UrlHelper::encodePath($path);
  310. $this->assertEquals($expected, $encoded);
  311. }
  312. /**
  313. * Provides data for self::testEncodePath().
  314. *
  315. * @return array
  316. */
  317. public static function providerTestEncodePath() {
  318. return [
  319. ['unencoded path with spaces', 'unencoded%20path%20with%20spaces'],
  320. ['slashes/should/be/preserved', 'slashes/should/be/preserved'],
  321. ];
  322. }
  323. /**
  324. * Tests external versus internal paths.
  325. *
  326. * @dataProvider providerTestIsExternal
  327. * @covers ::isExternal
  328. *
  329. * @param string $path
  330. * URL or path to test.
  331. * @param bool $expected
  332. * Expected result.
  333. */
  334. public function testIsExternal($path, $expected) {
  335. $isExternal = UrlHelper::isExternal($path);
  336. $this->assertEquals($expected, $isExternal);
  337. }
  338. /**
  339. * Provides data for self::testIsExternal().
  340. *
  341. * @return array
  342. */
  343. public static function providerTestIsExternal() {
  344. return [
  345. ['/internal/path', FALSE],
  346. ['https://example.com/external/path', TRUE],
  347. ['javascript://fake-external-path', FALSE],
  348. // External URL without an explicit protocol.
  349. ['//www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE],
  350. // Internal URL starting with a slash.
  351. ['/www.drupal.org', FALSE],
  352. // Simple external URLs.
  353. ['http://example.com', TRUE],
  354. ['https://example.com', TRUE],
  355. ['http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE],
  356. ['//drupal.org', TRUE],
  357. // Some browsers ignore or strip leading control characters.
  358. ["\x00//www.example.com", TRUE],
  359. ["\x08//www.example.com", TRUE],
  360. ["\x1F//www.example.com", TRUE],
  361. ["\n//www.example.com", TRUE],
  362. // JSON supports decoding directly from UTF-8 code points.
  363. [json_decode('"\u00AD"') . "//www.example.com", TRUE],
  364. [json_decode('"\u200E"') . "//www.example.com", TRUE],
  365. [json_decode('"\uE0020"') . "//www.example.com", TRUE],
  366. [json_decode('"\uE000"') . "//www.example.com", TRUE],
  367. // Backslashes should be normalized to forward.
  368. ['\\\\example.com', TRUE],
  369. // Local URLs.
  370. ['node', FALSE],
  371. ['/system/ajax', FALSE],
  372. ['?q=foo:bar', FALSE],
  373. ['node/edit:me', FALSE],
  374. ['/drupal.org', FALSE],
  375. ['<front>', FALSE],
  376. ];
  377. }
  378. /**
  379. * Tests bad protocol filtering and escaping.
  380. *
  381. * @dataProvider providerTestFilterBadProtocol
  382. * @covers ::setAllowedProtocols
  383. * @covers ::filterBadProtocol
  384. *
  385. * @param string $uri
  386. * Protocol URI.
  387. * @param string $expected
  388. * Expected escaped value.
  389. * @param array $protocols
  390. * Protocols to allow.
  391. */
  392. public function testFilterBadProtocol($uri, $expected, $protocols) {
  393. UrlHelper::setAllowedProtocols($protocols);
  394. $this->assertEquals($expected, UrlHelper::filterBadProtocol($uri));
  395. // Multiple calls to UrlHelper::filterBadProtocol() do not cause double
  396. // escaping.
  397. $this->assertEquals($expected, UrlHelper::filterBadProtocol(UrlHelper::filterBadProtocol($uri)));
  398. }
  399. /**
  400. * Provides data for self::testTestFilterBadProtocol().
  401. *
  402. * @return array
  403. */
  404. public static function providerTestFilterBadProtocol() {
  405. return [
  406. ['javascript://example.com?foo&bar', '//example.com?foo&amp;bar', ['http', 'https']],
  407. // Test custom protocols.
  408. ['http://example.com?foo&bar', '//example.com?foo&amp;bar', ['https']],
  409. // Valid protocol.
  410. ['http://example.com?foo&bar', 'http://example.com?foo&amp;bar', ['https', 'http']],
  411. // Colon not part of the URL scheme.
  412. ['/test:8888?foo&bar', '/test:8888?foo&amp;bar', ['http']],
  413. ];
  414. }
  415. /**
  416. * Tests dangerous url protocol filtering.
  417. *
  418. * @dataProvider providerTestStripDangerousProtocols
  419. * @covers ::setAllowedProtocols
  420. * @covers ::stripDangerousProtocols
  421. *
  422. * @param string $uri
  423. * Protocol URI.
  424. * @param string $expected
  425. * Expected escaped value.
  426. * @param array $protocols
  427. * Protocols to allow.
  428. */
  429. public function testStripDangerousProtocols($uri, $expected, $protocols) {
  430. UrlHelper::setAllowedProtocols($protocols);
  431. $stripped = UrlHelper::stripDangerousProtocols($uri);
  432. $this->assertEquals($expected, $stripped);
  433. }
  434. /**
  435. * Provides data for self::testStripDangerousProtocols().
  436. *
  437. * @return array
  438. */
  439. public static function providerTestStripDangerousProtocols() {
  440. return [
  441. ['javascript://example.com', '//example.com', ['http', 'https']],
  442. // Test custom protocols.
  443. ['http://example.com', '//example.com', ['https']],
  444. // Valid protocol.
  445. ['http://example.com', 'http://example.com', ['https', 'http']],
  446. // Colon not part of the URL scheme.
  447. ['/test:8888', '/test:8888', ['http']],
  448. ];
  449. }
  450. /**
  451. * Enhances test urls with schemes
  452. *
  453. * @param array $urls
  454. * The list of urls.
  455. *
  456. * @return array
  457. * A list of provider data with schemes.
  458. */
  459. protected function dataEnhanceWithScheme(array $urls) {
  460. $url_schemes = ['http', 'https', 'ftp'];
  461. $data = [];
  462. foreach ($url_schemes as $scheme) {
  463. foreach ($urls as $url) {
  464. $data[] = [$url, $scheme];
  465. }
  466. }
  467. return $data;
  468. }
  469. /**
  470. * Enhances test urls with prefixes.
  471. *
  472. * @param array $urls
  473. * The list of urls.
  474. *
  475. * @return array
  476. * A list of provider data with prefixes.
  477. */
  478. protected function dataEnhanceWithPrefix(array $urls) {
  479. $prefixes = ['', '/'];
  480. $data = [];
  481. foreach ($prefixes as $prefix) {
  482. foreach ($urls as $url) {
  483. $data[] = [$url, $prefix];
  484. }
  485. }
  486. return $data;
  487. }
  488. /**
  489. * Test detecting external urls that point to local resources.
  490. *
  491. * @param string $url
  492. * The external url to test.
  493. * @param string $base_url
  494. * The base url.
  495. * @param bool $expected
  496. * TRUE if an external URL points to this installation as determined by the
  497. * base url.
  498. *
  499. * @covers ::externalIsLocal
  500. * @dataProvider providerTestExternalIsLocal
  501. */
  502. public function testExternalIsLocal($url, $base_url, $expected) {
  503. $this->assertSame($expected, UrlHelper::externalIsLocal($url, $base_url));
  504. }
  505. /**
  506. * Provider for local external url detection.
  507. *
  508. * @see \Drupal\Tests\Component\Utility\UrlHelperTest::testExternalIsLocal()
  509. */
  510. public function providerTestExternalIsLocal() {
  511. return [
  512. // Different mixes of trailing slash.
  513. ['http://example.com', 'http://example.com', TRUE],
  514. ['http://example.com/', 'http://example.com', TRUE],
  515. ['http://example.com', 'http://example.com/', TRUE],
  516. ['http://example.com/', 'http://example.com/', TRUE],
  517. // Sub directory of site.
  518. ['http://example.com/foo', 'http://example.com/', TRUE],
  519. ['http://example.com/foo/bar', 'http://example.com/foo', TRUE],
  520. ['http://example.com/foo/bar', 'http://example.com/foo/', TRUE],
  521. // Different sub-domain.
  522. ['http://example.com', 'http://www.example.com/', FALSE],
  523. ['http://example.com/', 'http://www.example.com/', FALSE],
  524. ['http://example.com/foo', 'http://www.example.com/', FALSE],
  525. // Different TLD.
  526. ['http://example.com', 'http://example.ca', FALSE],
  527. ['http://example.com', 'http://example.ca/', FALSE],
  528. ['http://example.com/', 'http://example.ca/', FALSE],
  529. ['http://example.com/foo', 'http://example.ca', FALSE],
  530. ['http://example.com/foo', 'http://example.ca/', FALSE],
  531. // Different site path.
  532. ['http://example.com/foo', 'http://example.com/bar', FALSE],
  533. ['http://example.com', 'http://example.com/bar', FALSE],
  534. ['http://example.com/bar', 'http://example.com/bar/', FALSE],
  535. // Ensure \ is normalised to / since some browsers do that.
  536. ['http://www.example.ca\@example.com', 'http://example.com', FALSE],
  537. // Some browsers ignore or strip leading control characters.
  538. ["\x00//www.example.ca", 'http://example.com', FALSE],
  539. ];
  540. }
  541. /**
  542. * Test invalid url arguments.
  543. *
  544. * @param string $url
  545. * The url to test.
  546. * @param string $base_url
  547. * The base url.
  548. *
  549. * @covers ::externalIsLocal
  550. * @dataProvider providerTestExternalIsLocalInvalid
  551. */
  552. public function testExternalIsLocalInvalid($url, $base_url) {
  553. if (method_exists($this, 'expectException')) {
  554. $this->expectException(\InvalidArgumentException::class);
  555. }
  556. else {
  557. $this->setExpectedException(\InvalidArgumentException::class);
  558. }
  559. UrlHelper::externalIsLocal($url, $base_url);
  560. }
  561. /**
  562. * Provides invalid argument data for local external url detection.
  563. *
  564. * @see \Drupal\Tests\Component\Utility\UrlHelperTest::testExternalIsLocalInvalid()
  565. */
  566. public function providerTestExternalIsLocalInvalid() {
  567. return [
  568. ['http://example.com/foo', ''],
  569. ['http://example.com/foo', 'bar'],
  570. ['http://example.com/foo', 'http://'],
  571. // Invalid destination urls.
  572. ['', 'http://example.com/foo'],
  573. ['bar', 'http://example.com/foo'],
  574. ['/bar', 'http://example.com/foo'],
  575. ['bar/', 'http://example.com/foo'],
  576. ['http://', 'http://example.com/foo'],
  577. ];
  578. }
  579. }