Completed
Push — develop ( 209c03...240a1a )
by John
02:43
created

FrontController   D

Complexity

Total Complexity 34

Size/Duplication

Total Lines 553
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 29

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 34
c 3
b 0
f 0
lcom 1
cbo 29
dl 0
loc 553
rs 4.6

13 Methods

Rating   Name   Duplication   Size   Complexity  
A setEncrypt() 0 4 1
A generateSecureURL() 0 10 2
A encodeQuery() 0 8 1
C __construct() 0 195 7
A decodeQueryParams() 0 9 1
A getDecodeQueryParams() 0 18 2
A getPageController() 0 4 1
A registerFilter() 0 8 2
A getFilters() 0 4 1
A addRoute() 0 11 2
A value() 0 6 1
B getRouteCallback() 0 36 6
C process() 0 36 7
1
<?php
2
3
namespace Alpha\Controller\Front;
4
5
use Alpha\Util\Logging\Logger;
6
use Alpha\Util\Config\ConfigProvider;
7
use Alpha\Util\Security\SecurityUtils;
8
use Alpha\Util\Http\Filter\FilterInterface;
9
use Alpha\Util\Http\Response;
10
use Alpha\Util\Http\Request;
11
use Alpha\Exception\BadRequestException;
12
use Alpha\Exception\ResourceNotFoundException;
13
use Alpha\Exception\SecurityException;
14
use Alpha\Exception\IllegalArguementException;
15
use Alpha\Exception\AlphaException;
16
use Alpha\Controller\Controller;
17
use Alpha\Controller\ArticleController;
18
use Alpha\Controller\AttachmentController;
19
use Alpha\Controller\CacheController;
20
use Alpha\Controller\DEnumController;
21
use Alpha\Controller\ExcelController;
22
use Alpha\Controller\FeedController;
23
use Alpha\Controller\GenSecureQueryStringController;
24
use Alpha\Controller\ImageController;
25
use Alpha\Controller\ListActiveRecordsController;
26
use Alpha\Controller\LogController;
27
use Alpha\Controller\LoginController;
28
use Alpha\Controller\LogoutController;
29
use Alpha\Controller\MetricController;
30
use Alpha\Controller\RecordSelectorController;
31
use Alpha\Controller\SearchController;
32
use Alpha\Controller\SequenceController;
33
use Alpha\Controller\TagController;
34
use Alpha\Controller\IndexController;
35
use Alpha\Controller\InstallController;
36
use Alpha\Controller\ActiveRecordController;
37
use Alpha\Controller\PhpinfoController;
38
39
/**
40
 * The front controller designed to optionally handle all requests.
41
 *
42
 * @since 1.0
43
 *
44
 * @author John Collins <[email protected]>
45
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
46
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
47
 * All rights reserved.
48
 *
49
 * <pre>
50
 * Redistribution and use in source and binary forms, with or
51
 * without modification, are permitted provided that the
52
 * following conditions are met:
53
 *
54
 * * Redistributions of source code must retain the above
55
 *   copyright notice, this list of conditions and the
56
 *   following disclaimer.
57
 * * Redistributions in binary form must reproduce the above
58
 *   copyright notice, this list of conditions and the
59
 *   following disclaimer in the documentation and/or other
60
 *   materials provided with the distribution.
61
 * * Neither the name of the Alpha Framework nor the names
62
 *   of its contributors may be used to endorse or promote
63
 *   products derived from this software without specific
64
 *   prior written permission.
65
 *
66
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
67
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
68
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
69
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
70
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
71
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
72
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
73
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
74
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
75
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
76
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
77
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
78
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
79
 * </pre>
80
 */
81
class FrontController
82
{
83
    /**
84
     * The GET query string.
85
     *
86
     * @var string
87
     *
88
     * @since 1.0
89
     */
90
    private $queryString;
0 ignored issues
show
Unused Code introduced by
The property $queryString is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
91
92
    /**
93
     * The name of the page controller we want to invoke.
94
     *
95
     * @var string
96
     *
97
     * @since 1.0
98
     */
99
    private $pageController;
100
101
    /**
102
     * Boolean to flag if the GET query string is encrypted or not.
103
     *
104
     * @var bool
105
     *
106
     * @since 1.0
107
     */
108
    private $encryptedQuery = false;
109
110
    /**
111
     * An array of HTTP filters applied to each request to the front controller.  Each
112
     * member must implement FilterInterface!
113
     *
114
     * @var array
115
     *
116
     * @since 1.0
117
     */
118
    private $filters = array();
119
120
    /**
121
     * An associative array of URIs to callable methods to service matching requests.
122
     *
123
     * @var array
124
     *
125
     * @since 2.0
126
     */
127
    private $routes;
128
129
    /**
130
     * The route for the current request.
131
     *
132
     * @var string
133
     *
134
     * @since 2.0
135
     */
136
    private $currentRoute;
137
138
    /**
139
     * An optional 2D hash array of default request parameter values to use when those params are left off the request.
140
     *
141
     * @var array
142
     *
143
     * @since 2.0
144
     */
145
    private $defaultParamValues;
146
147
    /**
148
     * Trace logger.
149
     *
150
     * @var \Alpha\Util\Logging\Logger
151
     *
152
     * @since 1.0
153
     */
154
    private static $logger = null;
155
156
    /**
157
     * The constructor method.
158
     *
159
     * @throws \Alpha\Exception\BadRequestException
160
     *
161
     * @since 1.0
162
     */
163
    public function __construct()
164
    {
165
        self::$logger = new Logger('FrontController');
166
167
        self::$logger->debug('>>__construct()');
168
169
        $config = ConfigProvider::getInstance();
170
171
        mb_internal_encoding('UTF-8');
172
        mb_http_output('UTF-8');
173
        mb_http_input('UTF-8');
174
        if (!mb_check_encoding()) {
175
            throw new BadRequestException('Request character encoding does not match expected UTF-8');
176
        }
177
178
        $this->addRoute('/', function ($request) {
179
            $controller = new IndexController();
180
181
            return $controller->process($request);
182
        });
183
184
        $this->addRoute('/a/{title}/{view}', function ($request) {
185
            $controller = new ArticleController();
186
187
            return $controller->process($request);
188
        })->value('title', null)->value('view', 'detailed');
189
190
        $this->addRoute('/articles/{start}/{limit}', function ($request) {
191
            $controller = new ArticleController();
192
193
            return $controller->process($request);
194
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
195
196
        $this->addRoute('/attach/{articleID}/{filename}', function ($request) {
197
            $controller = new AttachmentController();
198
199
            return $controller->process($request);
200
        });
201
202
        $this->addRoute('/cache', function ($request) {
203
            $controller = new CacheController();
204
205
            return $controller->process($request);
206
        });
207
208
        $this->addRoute('/denum/{denumID}', function ($request) {
209
            $controller = new DEnumController();
210
211
            return $controller->process($request);
212
        })->value('denumID', null);
213
214
        $this->addRoute('/excel/{ActiveRecordType}/{ActiveRecordID}', function ($request) {
215
            $controller = new ExcelController();
216
217
            return $controller->process($request);
218
        })->value('ActiveRecordID', null);
219
220
        $this->addRoute('/feed/{ActiveRecordType}/{type}', function ($request) {
221
            $controller = new FeedController();
222
223
            return $controller->process($request);
224
        })->value('type', 'Atom');
225
226
        $this->addRoute('/gensecure', function ($request) {
227
            $controller = new GenSecureQueryStringController();
228
229
            return $controller->process($request);
230
        });
231
232
        $this->addRoute('/image/{source}/{width}/{height}/{type}/{quality}/{scale}/{secure}/{var1}/{var2}', function ($request) {
233
            $controller = new ImageController();
234
235
            return $controller->process($request);
236
        })->value('var1', null)->value('var2', null);
237
238
        $this->addRoute('/listactiverecords', function ($request) {
239
            $controller = new ListActiveRecordsController();
240
241
            return $controller->process($request);
242
        });
243
244
        $this->addRoute('/log/{logPath}', function ($request) {
245
            $controller = new LogController();
246
247
            return $controller->process($request);
248
        });
249
250
        $this->addRoute('/login', function ($request) {
251
            $controller = new LoginController();
252
253
            return $controller->process($request);
254
        });
255
256
        $this->addRoute('/logout', function ($request) {
257
            $controller = new LogoutController();
258
259
            return $controller->process($request);
260
        });
261
262
        $this->addRoute('/metric', function ($request) {
263
            $controller = new MetricController();
264
265
            return $controller->process($request);
266
        });
267
268
        $this->addRoute('/recordselector/12m/{ActiveRecordID}/{field}/{relatedClass}/{relatedClassField}/{relatedClassDisplayField}/{relationType}', function ($request) {
269
            $controller = new RecordSelectorController();
270
271
            return $controller->process($request);
272
        })->value('relationType', 'ONE-TO-MANY');
273
274
        $this->addRoute('/recordselector/m2m/{ActiveRecordID}/{field}/{relatedClassLeft}/{relatedClassLeftDisplayField}/{relatedClassRight}/{relatedClassRightDisplayField}/{accessingClassName}/{lookupIDs}/{relationType}', function ($request) {
275
            $controller = new RecordSelectorController();
276
277
            return $controller->process($request);
278
        })->value('relationType', 'MANY-TO-MANY');
279
280
        $this->addRoute('/search/{query}/{start}/{limit}', function ($request) {
281
            $controller = new SearchController();
282
283
            return $controller->process($request);
284
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
285
286
        $this->addRoute('/sequence/{start}/{limit}', function ($request) {
287
            $controller = new SequenceController();
288
289
            return $controller->process($request);
290
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
291
292
        $this->addRoute('/tag/{ActiveRecordType}/{ActiveRecordID}', function ($request) {
293
            $controller = new TagController();
294
295
            return $controller->process($request);
296
        });
297
298
        $this->addRoute('/install', function ($request) {
299
            $controller = new InstallController();
300
301
            return $controller->process($request);
302
        });
303
304
        $this->addRoute('/record/{ActiveRecordType}/{ActiveRecordID}/{view}', function ($request) {
305
            $controller = new ActiveRecordController();
306
307
            return $controller->process($request);
308
        })->value('ActiveRecordID', null)->value('view', 'detailed');
309
310
        $this->addRoute('/records/{ActiveRecordType}/{start}/{limit}', function ($request) {
311
            $controller = new ActiveRecordController();
312
313
            return $controller->process($request);
314
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
315
316
        $this->addRoute('/tk/{token}', function ($request) {
317
            $params = self::getDecodeQueryParams($request->getParam('token'));
318
319
            if (isset($params['act'])) {
320
                $className = $params['act'];
321
322
                if (class_exists($className)) {
323
                    $controller = new $className();
324
325
                    if (isset($params['ActiveRecordType']) && $params['act'] == 'Alpha\Controller\ActiveRecordController') {
326
                        $customController = $controller->getCustomControllerName($params['ActiveRecordType']);
327
                        if ($customController != null) {
328
                            $controller = new $customController();
329
                        }
330
                    }
331
332
                    $request->setParams(array_merge($params, $request->getParams()));
333
334
                    return $controller->process($request);
335
                }
336
            }
337
338
            self::$logger->warn('Bad params ['.print_r($params, true).'] provided on a /tk/ request');
339
340
            return new Response(404, 'Resource not found');
341
        });
342
343
        $this->addRoute('/alpha/service', function ($request) {
344
            $controller = new LoginController();
345
            $controller->setUnitOfWork(array('Alpha\Controller\LoginController', 'Alpha\Controller\ListActiveRecordsController'));
346
347
            return $controller->process($request);
348
        });
349
350
        $this->addRoute('/phpinfo', function ($request) {
351
            $controller = new PhpinfoController();
352
353
            return $controller->process($request);
354
        });
355
356
        self::$logger->debug('<<__construct');
357
    }
358
359
    /**
360
     * Sets the encryption flag.
361
     *
362
     * @param bool $encryptedQuery
363
     *
364
     * @since 1.0
365
     */
366
    public function setEncrypt($encryptedQuery)
367
    {
368
        $this->encryptedQuery = $encryptedQuery;
369
    }
370
371
    /**
372
     * Static method for generating an absolute, secure URL for a page controller.
373
     *
374
     * @param string $params
375
     *
376
     * @return string
377
     *
378
     * @since 1.0
379
     */
380
    public static function generateSecureURL($params)
381
    {
382
        $config = ConfigProvider::getInstance();
383
384
        if ($config->get('app.use.pretty.urls')) {
385
            return $config->get('app.url').'/tk/'.self::encodeQuery($params);
386
        } else {
387
            return $config->get('app.url').'?tk='.self::encodeQuery($params);
388
        }
389
    }
390
391
    /**
392
     * Static method for encoding a query string.
393
     *
394
     * @param string $queryString
395
     *
396
     * @return string
397
     *
398
     * @since 1.0
399
     */
400
    public static function encodeQuery($queryString)
401
    {
402
        $return = base64_encode(SecurityUtils::encrypt($queryString));
403
        // remove any characters that are likely to cause trouble on a URL
404
        $return = strtr($return, '+/', '-_');
405
406
        return $return;
407
    }
408
409
    /**
410
     * Static method to return the decoded GET paramters from an encrytpted tk value.
411
     *
412
     * @return string
413
     *
414
     * @since 1.0
415
     */
416
    public static function decodeQueryParams($tk)
417
    {
418
        // replace any troublesome characters from the URL with the original values
419
        $token = strtr($tk, '-_', '+/');
420
        $token = base64_decode($token);
421
        $params = SecurityUtils::decrypt($token);
422
423
        return $params;
424
    }
425
426
    /**
427
     * Static method to return the decoded GET paramters from an encrytpted tk value as an array of key/value pairs.
428
     *
429
     * @return array
430
     *
431
     * @since 1.0
432
     */
433
    public static function getDecodeQueryParams($tk)
434
    {
435
        // replace any troublesome characters from the URL with the original values
436
        $token = strtr($tk, '-_', '+/');
437
        $token = base64_decode($token);
438
        $params = SecurityUtils::decrypt($token);
439
440
        $pairs = explode('&', $params);
441
442
        $parameters = array();
443
444
        foreach ($pairs as $pair) {
445
            $split = explode('=', $pair);
446
            $parameters[$split[0]] = $split[1];
447
        }
448
449
        return $parameters;
450
    }
451
452
    /**
453
     * Getter for the page controller.
454
     *
455
     * @return string
456
     *
457
     * @since 1.0
458
     */
459
    public function getPageController()
460
    {
461
        return $this->pageController;
462
    }
463
464
    /**
465
     * Add the supplied filter object to the list of filters ran on each request to the front controller.
466
     *
467
     * @param \Alpha\Util\Http\Filter\FilterInterface $filterObject
468
     *
469
     * @throws \Alpha\Exception\IllegalArguementException
470
     *
471
     * @since 1.0
472
     */
473
    public function registerFilter($filterObject)
474
    {
475
        if ($filterObject instanceof FilterInterface) {
476
            array_push($this->filters, $filterObject);
477
        } else {
478
            throw new IllegalArguementException('Supplied filter object is not a valid FilterInterface instance!');
479
        }
480
    }
481
482
    /**
483
     * Returns the array of filters currently attached to this FrontController.
484
     *
485
     * @return array
486
     *
487
     * @since 1.0
488
     */
489
    public function getFilters()
490
    {
491
        return $this->filters;
492
    }
493
494
    /**
495
     * Add a new route to map a URI to the callback that will service its requests,
496
     * normally by invoking a controller class.
497
     *
498
     * @param string   $URI      The URL to match, can include params within curly {} braces.
499
     * @param callable $callback The method to service the matched requests (should return a Response!).
500
     *
501
     * @throws \Alpha\Exception\IllegalArguementException
502
     *
503
     * @return \Alpha\Controller\Front\FrontController
504
     *
505
     * @since 2.0
506
     */
507
    public function addRoute($URI, $callback)
508
    {
509
        if (is_callable($callback)) {
510
            $this->routes[$URI] = $callback;
511
            $this->currentRoute = $URI;
512
513
            return $this;
514
        } else {
515
            throw new IllegalArguementException('Callback provided for route ['.$URI.'] is not callable');
516
        }
517
    }
518
519
    /**
520
     * Method to allow the setting of default request param values to be used when they are left off the request URI.
521
     *
522
     * @param string $param        The param name (as defined on the route between {} braces)
523
     * @param mixed  $defaultValue The value to use
524
     *
525
     * @return \Alpha\Controller\Front\FrontController
526
     *
527
     * @since 2.0
528
     */
529
    public function value($param, $defaultValue)
530
    {
531
        $this->defaultParamValues[$this->currentRoute][$param] = $defaultValue;
532
533
        return $this;
534
    }
535
536
    /**
537
     * Get the defined callback in the routes array for the URI provided.
538
     *
539
     * @param string $URI The URI to search for.
540
     *
541
     * @return callable
542
     *
543
     * @throws \Alpha\Exception\IllegalArguementException
544
     *
545
     * @since 2.0
546
     */
547
    public function getRouteCallback($URI)
548
    {
549
        if (array_key_exists($URI, $this->routes)) { // direct hit due to URL containing no params
550
            $this->currentRoute = $URI;
551
552
            return $this->routes[$URI];
553
        } else { // we need to use a regex to match URIs with params
554
555
            // route URIs with params provided to callback
556
            foreach ($this->routes as $route => $callback) {
557
                $pattern = '#^'.$route.'$#s';
558
                $pattern = preg_replace('#\{\S+\}#', '\S+', $pattern);
559
560
                if (preg_match($pattern, $URI)) {
561
                    $this->currentRoute = $route;
0 ignored issues
show
Documentation Bug introduced by
It seems like $route can also be of type integer. However, the property $currentRoute is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
562
563
                    return $callback;
564
                }
565
            }
566
567
             // route URIs with params missing (will attempt to layer on defaults later on in Request class)
568
            foreach ($this->routes as $route => $callback) {
569
                $pattern = '#^'.$route.'$#s';
570
                $pattern = preg_replace('#\/\{\S+\}#', '\/?', $pattern);
571
572
                if (preg_match($pattern, $URI)) {
573
                    $this->currentRoute = $route;
574
575
                    return $callback;
576
                }
577
            }
578
        }
579
580
        // if we made it this far then no match was found
581
        throw new IllegalArguementException('No callback defined for URI ['.$URI.']');
582
    }
583
584
    /**
585
     * Processes the supplied request by invoking the callable defined matching the request's URI.
586
     *
587
     * @param \Alpha\Util\Http\Request $request The request to process
588
     *
589
     * @return \Alpha\Util\Http\Response
590
     *
591
     * @throws \Alpha\Exception\ResourceNotFoundException
592
     * @throws \Alpha\Exception\ResourceNotAllowedException
593
     * @throws \Alpha\Exception\AlphaException
594
     *
595
     * @since 2.0
596
     */
597
    public function process($request)
598
    {
599
        foreach ($this->filters as $filter) {
600
            $filter->process($request);
601
        }
602
603
        try {
604
            $callback = $this->getRouteCallback($request->getURI());
605
        } catch (IllegalArguementException $e) {
606
            self::$logger->info($e->getMessage());
607
            throw new ResourceNotFoundException('Resource not found');
608
        }
609
610
        if ($request->getURI() != $this->currentRoute) {
611
            if (isset($this->defaultParamValues[$this->currentRoute])) {
612
                $request->parseParamsFromRoute($this->currentRoute, $this->defaultParamValues[$this->currentRoute]);
613
            } else {
614
                $request->parseParamsFromRoute($this->currentRoute);
615
            }
616
        }
617
618
        try {
619
            $response = call_user_func($callback, $request);
620
        } catch (ResourceNotFoundException $rnfe) {
621
            self::$logger->info('ResourceNotFoundException throw, source message ['.$rnfe->getMessage().']');
622
623
            return new Response(404, $rnfe->getMessage());
624
        }
625
626
        if ($response instanceof Response) {
627
            return $response;
628
        } else {
629
            self::$logger->error('The callable defined for route ['.$request->getURI().'] does not return a Response object');
630
            throw new AlphaException('Unable to process request');
631
        }
632
    }
633
}
634