Completed
Push — develop ( 7bf045...1e8e59 )
by John
09:05 queued 02:09
created

Alpha/Controller/Front/FrontController.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\IllegalArguementException;
14
use Alpha\Exception\AlphaException;
15
use Alpha\Controller\Controller;
16
use Alpha\Controller\ArticleController;
17
use Alpha\Controller\AttachmentController;
18
use Alpha\Controller\CacheController;
19
use Alpha\Controller\DEnumController;
20
use Alpha\Controller\ExcelController;
21
use Alpha\Controller\FeedController;
22
use Alpha\Controller\GenSecureQueryStringController;
23
use Alpha\Controller\ImageController;
24
use Alpha\Controller\ListActiveRecordsController;
25
use Alpha\Controller\LogController;
26
use Alpha\Controller\LoginController;
27
use Alpha\Controller\LogoutController;
28
use Alpha\Controller\MetricController;
29
use Alpha\Controller\RecordSelectorController;
30
use Alpha\Controller\SearchController;
31
use Alpha\Controller\SequenceController;
32
use Alpha\Controller\TagController;
33
use Alpha\Controller\IndexController;
34
use Alpha\Controller\InstallController;
35
use Alpha\Controller\ActiveRecordController;
36
use Alpha\Controller\PhpinfoController;
37
38
/**
39
 * The front controller designed to optionally handle all requests.
40
 *
41
 * @since 1.0
42
 *
43
 * @author John Collins <[email protected]>
44
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
45
 * @copyright Copyright (c) 2018, John Collins (founder of Alpha Framework).
46
 * All rights reserved.
47
 *
48
 * <pre>
49
 * Redistribution and use in source and binary forms, with or
50
 * without modification, are permitted provided that the
51
 * following conditions are met:
52
 *
53
 * * Redistributions of source code must retain the above
54
 *   copyright notice, this list of conditions and the
55
 *   following disclaimer.
56
 * * Redistributions in binary form must reproduce the above
57
 *   copyright notice, this list of conditions and the
58
 *   following disclaimer in the documentation and/or other
59
 *   materials provided with the distribution.
60
 * * Neither the name of the Alpha Framework nor the names
61
 *   of its contributors may be used to endorse or promote
62
 *   products derived from this software without specific
63
 *   prior written permission.
64
 *
65
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
66
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
67
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
68
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
69
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
70
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
71
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
72
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
73
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
74
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
75
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
76
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
77
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
78
 * </pre>
79
 */
80
class FrontController
81
{
82
    /**
83
     * The GET query string.
84
     *
85
     * @var string
86
     *
87
     * @since 1.0
88
     */
89
    private $queryString;
90
91
    /**
92
     * The name of the page controller we want to invoke.
93
     *
94
     * @var string
95
     *
96
     * @since 1.0
97
     */
98
    private $pageController;
99
100
    /**
101
     * Boolean to flag if the GET query string is encrypted or not.
102
     *
103
     * @var bool
104
     *
105
     * @since 1.0
106
     */
107
    private $encryptedQuery = false;
108
109
    /**
110
     * An array of HTTP filters applied to each request to the front controller.  Each
111
     * member must implement FilterInterface!
112
     *
113
     * @var array
114
     *
115
     * @since 1.0
116
     */
117
    private $filters = array();
118
119
    /**
120
     * An associative array of URIs to callable methods to service matching requests.
121
     *
122
     * @var array
123
     *
124
     * @since 2.0
125
     */
126
    private $routes;
127
128
    /**
129
     * The route for the current request.
130
     *
131
     * @var string
132
     *
133
     * @since 2.0
134
     */
135
    private $currentRoute;
136
137
    /**
138
     * An optional 2D hash array of default request parameter values to use when those params are left off the request.
139
     *
140
     * @var array
141
     *
142
     * @since 2.0
143
     */
144
    private $defaultParamValues;
145
146
    /**
147
     * Trace logger.
148
     *
149
     * @var \Alpha\Util\Logging\Logger
150
     *
151
     * @since 1.0
152
     */
153
    private static $logger = null;
154
155
    /**
156
     * The constructor method.
157
     *
158
     * @throws \Alpha\Exception\BadRequestException
159
     *
160
     * @since 1.0
161
     */
162
    public function __construct()
163
    {
164
        self::$logger = new Logger('FrontController');
165
166
        self::$logger->debug('>>__construct()');
167
168
        $config = ConfigProvider::getInstance();
169
170
        mb_internal_encoding('UTF-8');
171
        mb_http_output('UTF-8');
172
        mb_http_input('UTF-8');
173
        if (!mb_check_encoding()) {
174
            throw new BadRequestException('Request character encoding does not match expected UTF-8');
175
        }
176
177
        $this->addRoute('/', function ($request) {
178
            $controller = new IndexController();
179
180
            return $controller->process($request);
181
        });
182
183
        $this->addRoute('/a/{title}/{view}', function ($request) {
184
            $controller = new ArticleController();
185
186
            return $controller->process($request);
187
        })->value('title', null)->value('view', 'detailed');
188
189
        $this->addRoute('/articles/{start}/{limit}', function ($request) {
190
            $controller = new ArticleController();
191
192
            return $controller->process($request);
193
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
194
195
        $this->addRoute('/attach/{articleID}/{filename}', function ($request) {
196
            $controller = new AttachmentController();
197
198
            return $controller->process($request);
199
        })->value('filename', null);
200
201
        $this->addRoute('/cache', function ($request) {
202
            $controller = new CacheController();
203
204
            return $controller->process($request);
205
        });
206
207
        $this->addRoute('/denum/{denumID}', function ($request) {
208
            $controller = new DEnumController();
209
210
            return $controller->process($request);
211
        })->value('denumID', null);
212
213
        $this->addRoute('/excel/{ActiveRecordType}/{ActiveRecordID}', function ($request) {
214
            $controller = new ExcelController();
215
216
            return $controller->process($request);
217
        })->value('ActiveRecordID', null);
218
219
        $this->addRoute('/feed/{ActiveRecordType}/{type}', function ($request) {
220
            $controller = new FeedController();
221
222
            return $controller->process($request);
223
        })->value('type', 'Atom');
224
225
        $this->addRoute('/gensecure', function ($request) {
226
            $controller = new GenSecureQueryStringController();
227
228
            return $controller->process($request);
229
        });
230
231
        $this->addRoute('/image/{source}/{width}/{height}/{type}/{quality}/{scale}/{secure}/{var1}/{var2}', function ($request) {
232
            $controller = new ImageController();
233
234
            return $controller->process($request);
235
        })->value('var1', null)->value('var2', null);
236
237
        $this->addRoute('/listactiverecords', function ($request) {
238
            $controller = new ListActiveRecordsController();
239
240
            return $controller->process($request);
241
        });
242
243
        $this->addRoute('/log/{logPath}', function ($request) {
244
            $controller = new LogController();
245
246
            return $controller->process($request);
247
        });
248
249
        $this->addRoute('/login', function ($request) {
250
            $controller = new LoginController();
251
252
            return $controller->process($request);
253
        });
254
255
        $this->addRoute('/logout', function ($request) {
256
            $controller = new LogoutController();
257
258
            return $controller->process($request);
259
        });
260
261
        $this->addRoute('/metric', function ($request) {
262
            $controller = new MetricController();
263
264
            return $controller->process($request);
265
        });
266
267
        $this->addRoute('/recordselector/12m/{ActiveRecordID}/{field}/{relatedClass}/{relatedClassField}/{relatedClassDisplayField}/{relationType}', function ($request) {
268
            $controller = new RecordSelectorController();
269
270
            return $controller->process($request);
271
        })->value('relationType', 'ONE-TO-MANY');
272
273
        $this->addRoute('/recordselector/m2m/{ActiveRecordID}/{field}/{relatedClassLeft}/{relatedClassLeftDisplayField}/{relatedClassRight}/{relatedClassRightDisplayField}/{accessingClassName}/{lookupIDs}/{relationType}', function ($request) {
274
            $controller = new RecordSelectorController();
275
276
            return $controller->process($request);
277
        })->value('relationType', 'MANY-TO-MANY');
278
279
        $this->addRoute('/search/{query}/{start}/{limit}', function ($request) {
280
            $controller = new SearchController();
281
282
            return $controller->process($request);
283
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
284
285
        $this->addRoute('/sequence/{start}/{limit}', function ($request) {
286
            $controller = new SequenceController();
287
288
            return $controller->process($request);
289
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
290
291
        $this->addRoute('/tag/{ActiveRecordType}/{ActiveRecordID}', function ($request) {
292
            $controller = new TagController();
293
294
            return $controller->process($request);
295
        });
296
297
        $this->addRoute('/install', function ($request) {
298
            $controller = new InstallController();
299
300
            return $controller->process($request);
301
        });
302
303
        $this->addRoute('/record/{ActiveRecordType}/{ActiveRecordID}/{view}', function ($request) {
304
            $controller = new ActiveRecordController();
305
306
            return $controller->process($request);
307
        })->value('ActiveRecordID', null)->value('view', 'detailed');
308
309
        $this->addRoute('/records/{ActiveRecordType}/{start}/{limit}', function ($request) {
310
            $controller = new ActiveRecordController();
311
312
            return $controller->process($request);
313
        })->value('start', 0)->value('limit', $config->get('app.list.page.amount'));
314
315
        $this->addRoute('/tk/{token}', function ($request) {
316
            $params = self::getDecodeQueryParams($request->getParam('token'));
317
318
            if (isset($params['act'])) {
319
                $className = $params['act'];
320
321
                if (class_exists($className)) {
322
                    $controller = new $className();
323
324
                    if (isset($params['ActiveRecordType']) && $params['act'] == 'Alpha\Controller\ActiveRecordController') {
325
                        $customController = $controller->getCustomControllerName($params['ActiveRecordType']);
326
                        if ($customController != null) {
327
                            $controller = new $customController();
328
                        }
329
                    }
330
331
                    $request->setParams(array_merge($params, $request->getParams()));
332
333
                    return $controller->process($request);
334
                }
335
            }
336
337
            self::$logger->warn('Bad params ['.print_r($params, true).'] provided on a /tk/ request');
338
339
            return new Response(404, 'Resource not found');
340
        });
341
342
        $this->addRoute('/alpha/service', function ($request) {
343
            $controller = new LoginController();
344
            $controller->setUnitOfWork(array('Alpha\Controller\LoginController', 'Alpha\Controller\ListActiveRecordsController'));
345
346
            return $controller->process($request);
347
        });
348
349
        $this->addRoute('/phpinfo', function ($request) {
350
            $controller = new PhpinfoController();
351
352
            return $controller->process($request);
353
        });
354
355
        self::$logger->debug('<<__construct');
356
    }
357
358
    /**
359
     * Sets the encryption flag.
360
     *
361
     * @param bool $encryptedQuery
362
     *
363
     * @since 1.0
364
     */
365
    public function setEncrypt($encryptedQuery)
366
    {
367
        $this->encryptedQuery = $encryptedQuery;
368
    }
369
370
    /**
371
     * Static method for generating an absolute, secure URL for a page controller.
372
     *
373
     * @param string $params
374
     *
375
     * @return string
376
     *
377
     * @since 1.0
378
     */
379
    public static function generateSecureURL($params)
380
    {
381
        $config = ConfigProvider::getInstance();
382
383
        if ($config->get('app.use.pretty.urls')) {
384
            return $config->get('app.url').'/tk/'.self::encodeQuery($params);
385
        } else {
386
            return $config->get('app.url').'?tk='.self::encodeQuery($params);
387
        }
388
    }
389
390
    /**
391
     * Static method for encoding a query string.
392
     *
393
     * @param string $queryString
394
     *
395
     * @return string
396
     *
397
     * @since 1.0
398
     */
399
    public static function encodeQuery($queryString)
400
    {
401
        $return = base64_encode(SecurityUtils::encrypt($queryString));
402
        // remove any characters that are likely to cause trouble on a URL
403
        $return = strtr($return, '+/', '-_');
404
405
        return $return;
406
    }
407
408
    /**
409
     * Static method to return the decoded GET paramters from an encrytpted tk value.
410
     *
411
     * @return string
412
     *
413
     * @since 1.0
414
     */
415
    public static function decodeQueryParams($tk)
416
    {
417
        // replace any troublesome characters from the URL with the original values
418
        $token = strtr($tk, '-_', '+/');
419
        $token = base64_decode($token);
420
        $params = SecurityUtils::decrypt($token);
421
422
        return $params;
423
    }
424
425
    /**
426
     * Static method to return the decoded GET paramters from an encrytpted tk value as an array of key/value pairs.
427
     *
428
     * @return array
429
     *
430
     * @since 1.0
431
     */
432
    public static function getDecodeQueryParams($tk)
433
    {
434
        // replace any troublesome characters from the URL with the original values
435
        $token = strtr($tk, '-_', '+/');
436
        $token = base64_decode($token);
437
        $params = SecurityUtils::decrypt($token);
438
439
        $pairs = explode('&', $params);
440
441
        $parameters = array();
442
443
        foreach ($pairs as $pair) {
444
            $split = explode('=', $pair);
445
            $parameters[$split[0]] = $split[1];
446
        }
447
448
        return $parameters;
449
    }
450
451
    /**
452
     * Getter for the page controller.
453
     *
454
     * @return string
455
     *
456
     * @since 1.0
457
     */
458
    public function getPageController()
459
    {
460
        return $this->pageController;
461
    }
462
463
    /**
464
     * Add the supplied filter object to the list of filters ran on each request to the front controller.
465
     *
466
     * @param \Alpha\Util\Http\Filter\FilterInterface $filterObject
467
     *
468
     * @throws \Alpha\Exception\IllegalArguementException
469
     *
470
     * @since 1.0
471
     */
472
    public function registerFilter($filterObject)
473
    {
474
        if ($filterObject instanceof FilterInterface) {
475
            array_push($this->filters, $filterObject);
476
        } else {
477
            throw new IllegalArguementException('Supplied filter object is not a valid FilterInterface instance!');
478
        }
479
    }
480
481
    /**
482
     * Returns the array of filters currently attached to this FrontController.
483
     *
484
     * @return array
485
     *
486
     * @since 1.0
487
     */
488
    public function getFilters()
489
    {
490
        return $this->filters;
491
    }
492
493
    /**
494
     * Add a new route to map a URI to the callback that will service its requests,
495
     * normally by invoking a controller class.
496
     *
497
     * @param string   $URI      The URL to match, can include params within curly {} braces.
498
     * @param callable $callback The method to service the matched requests (should return a Response!).
499
     *
500
     * @throws \Alpha\Exception\IllegalArguementException
501
     *
502
     * @return \Alpha\Controller\Front\FrontController
503
     *
504
     * @since 2.0
505
     */
506
    public function addRoute($URI, $callback)
507
    {
508
        if (is_callable($callback)) {
509
            $this->routes[$URI] = $callback;
510
            $this->currentRoute = $URI;
511
512
            return $this;
513
        } else {
514
            throw new IllegalArguementException('Callback provided for route ['.$URI.'] is not callable');
515
        }
516
    }
517
518
    /**
519
     * Method to allow the setting of default request param values to be used when they are left off the request URI.
520
     *
521
     * @param string $param        The param name (as defined on the route between {} braces)
522
     * @param mixed  $defaultValue The value to use
523
     *
524
     * @return \Alpha\Controller\Front\FrontController
525
     *
526
     * @since 2.0
527
     */
528
    public function value($param, $defaultValue)
529
    {
530
        $this->defaultParamValues[$this->currentRoute][$param] = $defaultValue;
531
532
        return $this;
533
    }
534
535
    /**
536
     * Get the defined callback in the routes array for the URI provided.
537
     *
538
     * @param string $URI The URI to search for.
539
     *
540
     * @return callable
541
     *
542
     * @throws \Alpha\Exception\IllegalArguementException
543
     *
544
     * @since 2.0
545
     */
546
    public function getRouteCallback($URI)
547
    {
548
        if (array_key_exists($URI, $this->routes)) { // direct hit due to URL containing no params
549
            $this->currentRoute = $URI;
550
551
            return $this->routes[$URI];
552
        } else { // we need to use a regex to match URIs with params
553
554
            // route URIs with params provided to callback
555
            foreach ($this->routes as $route => $callback) {
556
                $pattern = '#^'.$route.'$#s';
557
                $pattern = preg_replace('#\{\S+\}#', '\S+', $pattern);
558
559
                if (preg_match($pattern, $URI)) {
560
                    $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...
561
562
                    return $callback;
563
                }
564
            }
565
566
                // route URIs with params missing (will attempt to layer on defaults later on in Request class)
567
            foreach ($this->routes as $route => $callback) {
568
                $pattern = '#^'.$route.'$#s';
569
                $pattern = preg_replace('#\/\{\S+\}#', '\/?', $pattern);
570
571
                if (preg_match($pattern, $URI)) {
572
                    $this->currentRoute = $route;
573
574
                    return $callback;
575
                }
576
            }
577
        }
578
579
        // if we made it this far then no match was found
580
        throw new IllegalArguementException('No callback defined for URI ['.$URI.']');
581
    }
582
583
    /**
584
     * Processes the supplied request by invoking the callable defined matching the request's URI.
585
     *
586
     * @param \Alpha\Util\Http\Request $request The request to process
587
     *
588
     * @return \Alpha\Util\Http\Response
589
     *
590
     * @throws \Alpha\Exception\ResourceNotFoundException
591
     * @throws \Alpha\Exception\ResourceNotAllowedException
592
     * @throws \Alpha\Exception\AlphaException
593
     *
594
     * @since 2.0
595
     */
596
    public function process($request)
597
    {
598
        foreach ($this->filters as $filter) {
599
            $filter->process($request);
600
        }
601
602
        try {
603
            $callback = $this->getRouteCallback($request->getURI());
604
        } catch (IllegalArguementException $e) {
605
            self::$logger->info($e->getMessage());
606
            throw new ResourceNotFoundException('Resource not found');
607
        }
608
609
        if ($request->getURI() != $this->currentRoute) {
610
            if (isset($this->defaultParamValues[$this->currentRoute])) {
611
                $request->parseParamsFromRoute($this->currentRoute, $this->defaultParamValues[$this->currentRoute]);
612
            } else {
613
                $request->parseParamsFromRoute($this->currentRoute);
614
            }
615
        }
616
617
        try {
618
            $response = call_user_func($callback, $request);
619
        } catch (ResourceNotFoundException $rnfe) {
620
            self::$logger->info('ResourceNotFoundException throw, source message ['.$rnfe->getMessage().']');
621
622
            return new Response(404, $rnfe->getMessage());
623
        }
624
625
        if ($response instanceof Response) {
626
            return $response;
627
        } else {
628
            self::$logger->error('The callable defined for route ['.$request->getURI().'] does not return a Response object');
629
            throw new AlphaException('Unable to process request');
630
        }
631
    }
632
}
633