Completed
Pull Request — newinternal (#285)
by Simon
07:17 queued 04:17
created

RequestRouter::getRouteMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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