Failed Conditions
Branch newinternal (104de7)
by Simon
09:33
created

RequestRouter::getRouteFromPath()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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