Passed
Push — comment-flagging ( 6669bb...9d978e )
by Simon
06:56 queued 02:53
created

RequestRouter::getRouteMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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