Issues (186)

includes/Router/RequestRouter.php (1 issue)

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