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