Issues (195)

includes/Router/RequestRouter.php (1 issue)

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