Completed
Push — search ( 076ba7...2a5267 )
by Simon
16:36 queued 11:48
created

RequestRouter   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 442
Duplicated Lines 0 %

Test Coverage

Coverage 98%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 15
eloc 232
dl 0
loc 442
ccs 49
cts 50
cp 0.98
rs 10
c 2
b 0
f 0

6 Methods

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