Completed
Branch newinternal (ffe884)
by Simon
04:07
created

RequestRouter::getRouteFromPath()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 33
ccs 17
cts 17
cp 1
rs 8.439
c 1
b 0
f 0
cc 5
eloc 16
nc 6
nop 1
crap 5
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Router;
10
11
use Exception;
12
use Waca\Pages\Page404;
13
use Waca\Pages\PageBan;
14
use Waca\Pages\PageEditComment;
15
use Waca\Pages\PageEmailManagement;
16
use Waca\Pages\PageForgotPassword;
17
use Waca\Pages\PageLog;
18
use Waca\Pages\PageLogin;
19
use Waca\Pages\PageLogout;
20
use Waca\Pages\PageMain;
21
use Waca\Pages\PageOAuth;
22
use Waca\Pages\PagePreferences;
23
use Waca\Pages\PageRegister;
24
use Waca\Pages\PageSearch;
25
use Waca\Pages\PageSiteNotice;
26
use Waca\Pages\PageTeam;
27
use Waca\Pages\PageUserManagement;
28
use Waca\Pages\PageViewRequest;
29
use Waca\Pages\PageWelcomeTemplateManagement;
30
use Waca\Pages\RequestAction\PageBreakReservation;
31
use Waca\Pages\RequestAction\PageCloseRequest;
32
use Waca\Pages\RequestAction\PageComment;
33
use Waca\Pages\RequestAction\PageCustomClose;
34
use Waca\Pages\RequestAction\PageDeferRequest;
35
use Waca\Pages\RequestAction\PageDropRequest;
36
use Waca\Pages\RequestAction\PageReservation;
37
use Waca\Pages\RequestAction\PageSendToUser;
38
use Waca\Pages\Statistics\StatsFastCloses;
39
use Waca\Pages\Statistics\StatsInactiveUsers;
40
use Waca\Pages\Statistics\StatsMain;
41
use Waca\Pages\Statistics\StatsMonthlyStats;
42
use Waca\Pages\Statistics\StatsReservedRequests;
43
use Waca\Pages\Statistics\StatsTemplateStats;
44
use Waca\Pages\Statistics\StatsTopCreators;
45
use Waca\Pages\Statistics\StatsUsers;
46
use Waca\Tasks\IRoutedTask;
47
use Waca\WebRequest;
48
49
/**
50
 * Request router
51
 * @package  Waca\Router
52
 * @category Security-Critical
53
 */
54
class RequestRouter implements IRequestRouter
55
{
56
	/**
57
	 * This is the core routing table for the application. The basic idea is:
58
	 *
59
	 *      array(
60
	 *          "foo" =>
61
	 *              array(
62
	 *                  "class"   => PageFoo::class,
63
	 *                  "actions" => array("bar", "other")
64
	 *              ),
65
	 * );
66
	 *
67
	 * Things to note:
68
	 *     - If no page is requested, we go to PageMain. PageMain can't have actions defined.
69
	 *
70
	 *     - If a page is defined and requested, but no action is requested, go to that page's main() method
71
	 *     - If a page is defined and requested, and an action is defined and requested, go to that action's method.
72
	 *     - If a page is defined and requested, and an action NOT defined and requested, go to Page404 and it's main()
73
	 *       method.
74
	 *     - If a page is NOT defined and requested, go to Page404 and it's main() method.
75
	 *
76
	 *     - Query parameters are ignored.
77
	 *
78
	 * The key point here is request routing with validation that this is allowed, before we start hitting the
79
	 * filesystem through the AutoLoader, and opening random files. Also, so that we validate the action requested
80
	 * before we start calling random methods through the web UI.
81
	 *
82
	 * Examples:
83
	 * /internal.php                => returns instance of PageMain, routed to main()
84
	 * /internal.php?query          => returns instance of PageMain, routed to main()
85
	 * /internal.php/foo            => returns instance of PageFoo, routed to main()
86
	 * /internal.php/foo?query      => returns instance of PageFoo, routed to main()
87
	 * /internal.php/foo/bar        => returns instance of PageFoo, routed to bar()
88
	 * /internal.php/foo/bar?query  => returns instance of PageFoo, routed to bar()
89
	 * /internal.php/foo/baz        => returns instance of Page404, routed to main()
90
	 * /internal.php/foo/baz?query  => returns instance of Page404, routed to main()
91
	 * /internal.php/bar            => returns instance of Page404, routed to main()
92
	 * /internal.php/bar?query      => returns instance of Page404, routed to main()
93
	 * /internal.php/bar/baz        => returns instance of Page404, routed to main()
94
	 * /internal.php/bar/baz?query  => returns instance of Page404, routed to main()
95
	 *
96
	 * Take care when changing this - a lot of places rely on the array key for redirects and other links. If you need
97
	 * to change the key, then you'll likely have to update a lot of files.
98
	 *
99
	 * @var array
100
	 */
101
	private $routeMap = array(
102
103
		//////////////////////////////////////////////////////////////////////////////////////////////////
104
		// Login and registration
105
		'logout'                      =>
106
			array(
107
				'class'   => PageLogout::class,
108
				'actions' => array(),
109
			),
110
		'login'                       =>
111
			array(
112
				'class'   => PageLogin::class,
113
				'actions' => array(),
114
			),
115
		'forgotPassword'              =>
116
			array(
117
				'class'   => PageForgotPassword::class,
118
				'actions' => array('reset'),
119
			),
120
		'register'                    =>
121
			array(
122
				'class'   => PageRegister::class,
123
				'actions' => array('done'),
124
			),
125
126
		//////////////////////////////////////////////////////////////////////////////////////////////////
127
		// Discovery
128
		'search'                      =>
129
			array(
130
				'class'   => PageSearch::class,
131
				'actions' => array(),
132
			),
133
		'logs'                        =>
134
			array(
135
				'class'   => PageLog::class,
136
				'actions' => array(),
137
			),
138
139
		//////////////////////////////////////////////////////////////////////////////////////////////////
140
		// Administration
141
		'bans'                        =>
142
			array(
143
				'class'   => PageBan::class,
144
				'actions' => array('set', 'remove'),
145
			),
146
		'userManagement'              =>
147
			array(
148
				'class'   => PageUserManagement::class,
149
				'actions' => array(
150
					'approve',
151
					'decline',
152
					'rename',
153
					'editUser',
154
					'suspend',
155
					'promote',
156
					'demote',
157
				),
158
			),
159
		'siteNotice'                  =>
160
			array(
161
				'class'   => PageSiteNotice::class,
162
				'actions' => array(),
163
			),
164
		'emailManagement'             =>
165
			array(
166
				'class'   => PageEmailManagement::class,
167
				'actions' => array('create', 'edit', 'view'),
168
			),
169
170
		//////////////////////////////////////////////////////////////////////////////////////////////////
171
		// Personal preferences
172
		'preferences'                 =>
173
			array(
174
				'class'   => PagePreferences::class,
175
				'actions' => array('changePassword'),
176
			),
177
		'oauth'                       =>
178
			array(
179
				'class'   => PageOAuth::class,
180
				'actions' => array('detach', 'attach'),
181
			),
182
183
		//////////////////////////////////////////////////////////////////////////////////////////////////
184
		// Welcomer configuration
185
		'welcomeTemplates'            =>
186
			array(
187
				'class'   => PageWelcomeTemplateManagement::class,
188
				'actions' => array('select', 'edit', 'delete', 'add', 'view'),
189
			),
190
191
		//////////////////////////////////////////////////////////////////////////////////////////////////
192
		// Statistics
193
		'statistics'                  =>
194
			array(
195
				'class'   => StatsMain::class,
196
				'actions' => array(),
197
			),
198
		'statistics/fastCloses'       =>
199
			array(
200
				'class'   => StatsFastCloses::class,
201
				'actions' => array(),
202
			),
203
		'statistics/inactiveUsers'    =>
204
			array(
205
				'class'   => StatsInactiveUsers::class,
206
				'actions' => array(),
207
			),
208
		'statistics/monthlyStats'     =>
209
			array(
210
				'class'   => StatsMonthlyStats::class,
211
				'actions' => array(),
212
			),
213
		'statistics/reservedRequests' =>
214
			array(
215
				'class'   => StatsReservedRequests::class,
216
				'actions' => array(),
217
			),
218
		'statistics/templateStats'    =>
219
			array(
220
				'class'   => StatsTemplateStats::class,
221
				'actions' => array(),
222
			),
223
		'statistics/topCreators'      =>
224
			array(
225
				'class'   => StatsTopCreators::class,
226
				'actions' => array(),
227
			),
228
		'statistics/users'            =>
229
			array(
230
				'class'   => StatsUsers::class,
231
				'actions' => array('detail'),
232
			),
233
234
		//////////////////////////////////////////////////////////////////////////////////////////////////
235
		// Zoom page
236
		'viewRequest'                 =>
237
			array(
238
				'class'   => PageViewRequest::class,
239
				'actions' => array(),
240
			),
241
		'viewRequest/reserve'         =>
242
			array(
243
				'class'   => PageReservation::class,
244
				'actions' => array(),
245
			),
246
		'viewRequest/breakReserve'    =>
247
			array(
248
				'class'   => PageBreakReservation::class,
249
				'actions' => array(),
250
			),
251
		'viewRequest/defer'           =>
252
			array(
253
				'class'   => PageDeferRequest::class,
254
				'actions' => array(),
255
			),
256
		'viewRequest/comment'         =>
257
			array(
258
				'class'   => PageComment::class,
259
				'actions' => array(),
260
			),
261
		'viewRequest/sendToUser'      =>
262
			array(
263
				'class'   => PageSendToUser::class,
264
				'actions' => array(),
265
			),
266
		'viewRequest/close'           =>
267
			array(
268
				'class'   => PageCloseRequest::class,
269
				'actions' => array(),
270
			),
271
		'viewRequest/drop'            =>
272
			array(
273
				'class'   => PageDropRequest::class,
274
				'actions' => array(),
275
			),
276
		'viewRequest/custom'          =>
277
			array(
278
				'class'   => PageCustomClose::class,
279
				'actions' => array(),
280
			),
281
		'editComment'                 =>
282
			array(
283
				'class'   => PageEditComment::class,
284
				'actions' => array(),
285
			),
286
287
		//////////////////////////////////////////////////////////////////////////////////////////////////
288
		// Misc stuff
289
		'team'                        =>
290
			array(
291
				'class'   => PageTeam::class,
292
				'actions' => array(),
293
			),
294
295
	);
296
297
	/**
298
	 * @return IRoutedTask
299
	 * @throws Exception
300
	 */
301 5
	final public function route()
302
	{
303 5
		$pathInfo = WebRequest::pathInfo();
304
305 5
		list($pageClass, $action) = $this->getRouteFromPath($pathInfo);
306
307
		/** @var IRoutedTask $page */
308 5
		$page = new $pageClass();
309
310
		// Dynamic creation, so we've got to be careful here. We can't use built-in language type protection, so
311
		// let's use our own.
312 5
		if (!($page instanceof IRoutedTask)) {
313
			throw new Exception('Expected a page, but this is not a page.');
314
		}
315
316
		// OK, I'm happy at this point that we know we're running a page, and we know it's probably what we want if it
317
		// inherits PageBase and has been created from the routing map.
318 5
		$page->setRoute($action);
319
320 5
		return $page;
321
	}
322
323
	/**
324
	 * @param $pathInfo
325
	 *
326
	 * @return array
327
	 */
328 12
	protected function getRouteFromPath($pathInfo)
329
	{
330 12
		if (count($pathInfo) === 0) {
331
			// No pathInfo, so no page to load. Load the main page.
332 1
			return $this->getDefaultRoute();
333
		}
334 11
		elseif (count($pathInfo) === 1) {
335
			// Exactly one path info segment, it's got to be a page.
336 2
			$classSegment = $pathInfo[0];
337
338 2
			return $this->routeSinglePathSegment($classSegment);
339
		}
340
341
		// OK, we have two or more segments now.
342 9
		if (count($pathInfo) > 2) {
343
			// Let's handle more than two, and collapse it down into two.
344 2
			$requestedAction = array_pop($pathInfo);
345 2
			$classSegment = implode('/', $pathInfo);
346 2
		}
347
		else {
348
			// Two path info segments.
349 7
			$classSegment = $pathInfo[0];
350 7
			$requestedAction = $pathInfo[1];
351
		}
352
353 9
		$routeMap = $this->routePathSegments($classSegment, $requestedAction);
354
355 9
		if ($routeMap[0] === Page404::class) {
356 5
			$routeMap = $this->routeSinglePathSegment($classSegment . '/' . $requestedAction);
357 5
		}
358
359 9
		return $routeMap;
360
	}
361
362
	/**
363
	 * @param $classSegment
364
	 *
365
	 * @return array
366
	 */
367 7
	final protected function routeSinglePathSegment($classSegment)
368
	{
369 7
		$routeMap = $this->getRouteMap();
370 7
		if (array_key_exists($classSegment, $routeMap)) {
371
			// Route exists, but we don't have an action in path info, so default to main.
372 3
			$pageClass = $routeMap[$classSegment]['class'];
373 3
			$action = 'main';
374
375 3
			return array($pageClass, $action);
376
		}
377
		else {
378
			// Doesn't exist in map. Fall back to 404
379 4
			$pageClass = Page404::class;
380 4
			$action = "main";
381
382 4
			return array($pageClass, $action);
383
		}
384
	}
385
386
	/**
387
	 * @param $classSegment
388
	 * @param $requestedAction
389
	 *
390
	 * @return array
391
	 */
392 9
	final protected function routePathSegments($classSegment, $requestedAction)
393
	{
394 9
		$routeMap = $this->getRouteMap();
395 9
		if (array_key_exists($classSegment, $routeMap)) {
396
			// Route exists, but we don't have an action in path info, so default to main.
397
398 6
			if (isset($routeMap[$classSegment]['actions'])
399 6
				&& array_search($requestedAction, $routeMap[$classSegment]['actions']) !== false
400 6
			) {
401
				// Action exists in allowed action list. Allow both the page and the action
402 4
				$pageClass = $routeMap[$classSegment]['class'];
403 4
				$action = $requestedAction;
404
405 4
				return array($pageClass, $action);
406
			}
407
			else {
408
				// Valid page, invalid action. 404 our way out.
409 2
				$pageClass = Page404::class;
410 2
				$action = 'main';
411
412 2
				return array($pageClass, $action);
413
			}
414
		}
415
		else {
416
			// Class doesn't exist in map. Fall back to 404
417 3
			$pageClass = Page404::class;
418 3
			$action = 'main';
419
420 3
			return array($pageClass, $action);
421
		}
422
	}
423
424
	/**
425
	 * @return array
426
	 */
427 11
	protected function getRouteMap()
428
	{
429 11
		return $this->routeMap;
430
	}
431
432
	/**
433
	 * @return callable
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
434
	 */
435 1
	protected function getDefaultRoute()
436
	{
437 1
		return array(PageMain::class, "main");
438
	}
439
}