PathRouterTest.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * Tests for the PathRouter parsing.
  4. *
  5. * @covers PathRouter
  6. */
  7. class PathRouterTest extends MediaWikiTestCase {
  8. /**
  9. * @var PathRouter
  10. */
  11. protected $basicRouter;
  12. protected function setUp() {
  13. parent::setUp();
  14. $router = new PathRouter;
  15. $router->add( "/wiki/$1" );
  16. $this->basicRouter = $router;
  17. }
  18. public static function provideParse() {
  19. $tests = [
  20. // Basic path parsing
  21. 'Basic path parsing' => [
  22. "/wiki/$1",
  23. "/wiki/Foo",
  24. [ 'title' => "Foo" ]
  25. ],
  26. //
  27. 'Loose path auto-$1: /$1' => [
  28. "/",
  29. "/Foo",
  30. [ 'title' => "Foo" ]
  31. ],
  32. 'Loose path auto-$1: /wiki' => [
  33. "/wiki",
  34. "/wiki/Foo",
  35. [ 'title' => "Foo" ]
  36. ],
  37. 'Loose path auto-$1: /wiki/' => [
  38. "/wiki/",
  39. "/wiki/Foo",
  40. [ 'title' => "Foo" ]
  41. ],
  42. // Ensure that path is based on specificity, not order
  43. 'Order, /$1 added first' => [
  44. [ "/$1", "/a/$1", "/b/$1" ],
  45. "/a/Foo",
  46. [ 'title' => "Foo" ]
  47. ],
  48. 'Order, /$1 added last' => [
  49. [ "/b/$1", "/a/$1", "/$1" ],
  50. "/a/Foo",
  51. [ 'title' => "Foo" ]
  52. ],
  53. // Handling of key based arrays with a url parameter
  54. 'Key based array' => [
  55. [ [
  56. 'path' => [ 'edit' => "/edit/$1" ],
  57. 'params' => [ 'action' => '$key' ],
  58. ] ],
  59. "/edit/Foo",
  60. [ 'title' => "Foo", 'action' => 'edit' ]
  61. ],
  62. // Additional parameter
  63. 'Basic $2' => [
  64. [ [
  65. 'path' => '/$2/$1',
  66. 'params' => [ 'test' => '$2' ]
  67. ] ],
  68. "/asdf/Foo",
  69. [ 'title' => "Foo", 'test' => 'asdf' ]
  70. ],
  71. ];
  72. // Shared patterns for restricted value parameter tests
  73. $restrictedPatterns = [
  74. [
  75. 'path' => '/$2/$1',
  76. 'params' => [ 'test' => '$2' ],
  77. 'options' => [ '$2' => [ 'a', 'b' ] ]
  78. ],
  79. [
  80. 'path' => '/$2/$1',
  81. 'params' => [ 'test2' => '$2' ],
  82. 'options' => [ '$2' => 'c' ]
  83. ],
  84. '/$1'
  85. ];
  86. $tests += [
  87. // Restricted value parameter tests
  88. 'Restricted 1' => [
  89. $restrictedPatterns,
  90. "/asdf/Foo",
  91. [ 'title' => "asdf/Foo" ]
  92. ],
  93. 'Restricted 2' => [
  94. $restrictedPatterns,
  95. "/a/Foo",
  96. [ 'title' => "Foo", 'test' => 'a' ]
  97. ],
  98. 'Restricted 3' => [
  99. $restrictedPatterns,
  100. "/c/Foo",
  101. [ 'title' => "Foo", 'test2' => 'c' ]
  102. ],
  103. // Callback test
  104. 'Callback' => [
  105. [ [
  106. 'path' => "/$1",
  107. 'params' => [ 'a' => 'b', 'data:foo' => 'bar' ],
  108. 'options' => [ 'callback' => [ __CLASS__, 'callbackForTest' ] ]
  109. ] ],
  110. '/Foo',
  111. [
  112. 'title' => "Foo",
  113. 'x' => 'Foo',
  114. 'a' => 'b',
  115. 'foo' => 'bar'
  116. ]
  117. ],
  118. // Test to ensure that matches are not made if a parameter expects nonexistent input
  119. 'Fail' => [
  120. [ [
  121. 'path' => "/wiki/$1",
  122. 'params' => [ 'title' => "$1$2" ],
  123. ] ],
  124. "/wiki/A",
  125. []
  126. ],
  127. // Make sure the router handles titles like Special:Recentchanges correctly
  128. 'Special title' => [
  129. "/wiki/$1",
  130. "/wiki/Special:Recentchanges",
  131. [ 'title' => "Special:Recentchanges" ]
  132. ],
  133. // Make sure the router decodes urlencoding properly
  134. 'URL encoding' => [
  135. "/wiki/$1",
  136. "/wiki/Title_With%20Space",
  137. [ 'title' => "Title_With Space" ]
  138. ],
  139. // Double slash and dot expansion
  140. 'Double slash in prefix' => [
  141. '/wiki/$1',
  142. '//wiki/Foo',
  143. [ 'title' => 'Foo' ]
  144. ],
  145. 'Double slash at start of $1' => [
  146. '/wiki/$1',
  147. '/wiki//Foo',
  148. [ 'title' => '/Foo' ]
  149. ],
  150. 'Double slash in middle of $1' => [
  151. '/wiki/$1',
  152. '/wiki/.hack//SIGN',
  153. [ 'title' => '.hack//SIGN' ]
  154. ],
  155. 'Dots removed 1' => [
  156. '/wiki/$1',
  157. '/x/../wiki/Foo',
  158. [ 'title' => 'Foo' ]
  159. ],
  160. 'Dots removed 2' => [
  161. '/wiki/$1',
  162. '/./wiki/Foo',
  163. [ 'title' => 'Foo' ]
  164. ],
  165. 'Dots retained 1' => [
  166. '/wiki/$1',
  167. '/wiki/../wiki/Foo',
  168. [ 'title' => '../wiki/Foo' ]
  169. ],
  170. 'Dots retained 2' => [
  171. '/wiki/$1',
  172. '/wiki/./Foo',
  173. [ 'title' => './Foo' ]
  174. ],
  175. 'Triple slash' => [
  176. '/wiki/$1',
  177. '///wiki/Foo',
  178. [ 'title' => 'Foo' ]
  179. ],
  180. // '..' only traverses one slash, see e.g. RFC 3986
  181. 'Dots traversing double slash 1' => [
  182. '/wiki/$1',
  183. '/a//b/../../wiki/Foo',
  184. []
  185. ],
  186. 'Dots traversing double slash 2' => [
  187. '/wiki/$1',
  188. '/a//b/../../../wiki/Foo',
  189. [ 'title' => 'Foo' ]
  190. ],
  191. ];
  192. // Make sure the router doesn't break on special characters like $ used in regexp replacements
  193. foreach ( [ "$", "$1", "\\", "\\$1" ] as $char ) {
  194. $tests["Regexp character $char"] = [
  195. "/wiki/$1",
  196. "/wiki/$char",
  197. [ 'title' => "$char" ]
  198. ];
  199. }
  200. $tests += [
  201. // Make sure the router handles characters like +&() properly
  202. "Special characters" => [
  203. "/wiki/$1",
  204. "/wiki/Plus+And&Dollar\\Stuff();[]{}*",
  205. [ 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ],
  206. ],
  207. // Make sure the router handles unicode characters correctly
  208. "Unicode 1" => [
  209. "/wiki/$1",
  210. "/wiki/Spécial:Modifications_récentes" ,
  211. [ 'title' => "Spécial:Modifications_récentes" ],
  212. ],
  213. "Unicode 2" => [
  214. "/wiki/$1",
  215. "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes",
  216. [ 'title' => "Spécial:Modifications_récentes" ],
  217. ]
  218. ];
  219. // Ensure the router doesn't choke on long paths.
  220. $lorem = "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_" .
  221. "tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_" .
  222. "nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._" .
  223. "Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_" .
  224. "eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_" .
  225. "in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum.";
  226. $tests += [
  227. "Long path" => [
  228. "/wiki/$1",
  229. "/wiki/$lorem",
  230. [ 'title' => $lorem ]
  231. ],
  232. // Ensure that the php passed site of parameter values are not urldecoded
  233. "Pattern urlencoding" => [
  234. [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => '%20:$1' ] ] ],
  235. "/wiki/Foo",
  236. [ 'title' => '%20:Foo' ]
  237. ],
  238. // Ensure that raw parameter values do not have any variable replacements or urldecoding
  239. "Raw param value" => [
  240. [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => [ 'value' => 'bar%20$1' ] ] ] ],
  241. "/wiki/Foo",
  242. [ 'title' => 'bar%20$1' ]
  243. ]
  244. ];
  245. return $tests;
  246. }
  247. /**
  248. * Test path parsing
  249. * @dataProvider provideParse
  250. */
  251. public function testParse( $patterns, $path, $expected ) {
  252. $patterns = (array)$patterns;
  253. $router = new PathRouter;
  254. foreach ( $patterns as $pattern ) {
  255. if ( is_array( $pattern ) ) {
  256. $router->add( $pattern['path'], $pattern['params'] ?? [],
  257. $pattern['options'] ?? [] );
  258. } else {
  259. $router->add( $pattern );
  260. }
  261. }
  262. $matches = $router->parse( $path );
  263. $this->assertEquals( $matches, $expected );
  264. }
  265. public static function callbackForTest( &$matches, $data ) {
  266. $matches['x'] = $data['$1'];
  267. $matches['foo'] = $data['foo'];
  268. }
  269. public static function provideWeight() {
  270. return [
  271. [ '/Foo', [ 'title' => 'Foo' ] ],
  272. [ '/Bar', [ 'ping' => 'pong' ] ],
  273. [ '/Baz', [ 'marco' => 'polo' ] ],
  274. [ '/asdf-foo', [ 'title' => 'qwerty-foo' ] ],
  275. [ '/qwerty-bar', [ 'title' => 'asdf-bar' ] ],
  276. [ '/a/Foo', [ 'title' => 'Foo' ] ],
  277. [ '/asdf/Foo', [ 'title' => 'Foo' ] ],
  278. [ '/qwerty/Foo', [ 'title' => 'Foo', 'qwerty' => 'qwerty' ] ],
  279. [ '/baz/Foo', [ 'title' => 'Foo', 'unrestricted' => 'baz' ] ],
  280. [ '/y/Foo', [ 'title' => 'Foo', 'restricted-to-y' => 'y' ] ],
  281. ];
  282. }
  283. /**
  284. * Test to ensure weight of paths is handled correctly
  285. * @dataProvider provideWeight
  286. */
  287. public function testWeight( $path, $expected ) {
  288. $router = new PathRouter;
  289. $router->addStrict( "/Bar", [ 'ping' => 'pong' ] );
  290. $router->add( "/asdf-$1", [ 'title' => 'qwerty-$1' ] );
  291. $router->add( "/$1" );
  292. $router->add( "/qwerty-$1", [ 'title' => 'asdf-$1' ] );
  293. $router->addStrict( "/Baz", [ 'marco' => 'polo' ] );
  294. $router->add( "/a/$1" );
  295. $router->add( "/asdf/$1" );
  296. $router->add( "/$2/$1", [ 'unrestricted' => '$2' ] );
  297. $router->add( [ 'qwerty' => "/qwerty/$1" ], [ 'qwerty' => '$key' ] );
  298. $router->add( "/$2/$1", [ 'restricted-to-y' => '$2' ], [ '$2' => 'y' ] );
  299. $this->assertEquals( $router->parse( $path ), $expected );
  300. }
  301. }