Passed
Push — master ( c5ae54...f38dde )
by Fran
02:16
created

Router::sentAuthHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
namespace PSFS\base;
3
4
use Exception;
5
use InvalidArgumentException;
6
use PSFS\base\config\Config;
7
use PSFS\base\dto\JsonResponse;
8
use PSFS\base\exception\AccessDeniedException;
9
use PSFS\base\exception\AdminCredentialsException;
10
use PSFS\base\exception\ConfigException;
11
use PSFS\base\exception\GeneratorException;
12
use PSFS\base\exception\RouterException;
13
use PSFS\base\types\Controller;
14
use PSFS\base\types\helpers\AnnotationHelper;
15
use PSFS\base\types\helpers\GeneratorHelper;
16
use PSFS\base\types\helpers\I18nHelper;
17
use PSFS\base\types\helpers\Inspector;
18
use PSFS\base\types\helpers\RouterHelper;
19
use PSFS\base\types\helpers\SecurityHelper;
20
use PSFS\base\types\traits\SingletonTrait;
21
use PSFS\controller\base\Admin;
22
use ReflectionClass;
23
use ReflectionException;
24
use ReflectionMethod;
25
use Symfony\Component\Finder\Finder;
26
use Symfony\Component\Finder\SplFileInfo;
27
28
/**
29
 * Class Router
30
 * @package PSFS
31
 */
32
class Router
33
{
34
    use SingletonTrait;
35
36
    /**
37
     * @var array
38
     */
39
    private $routing = [];
40
    /**
41
     * @var array
42
     */
43
    private $slugs = [];
44
    /**
45
     * @var array
46
     */
47
    private $domains = [];
48
    /**
49
     * @var Finder $finder
50
     */
51
    private $finder;
52
    /**
53
     * @var Cache $cache
54
     */
55
    private $cache;
56
    /**
57
     * @var int
58
     */
59
    protected $cacheType = Cache::JSON;
60
61
    /**
62
     * Router constructor.
63
     * @throws GeneratorException
64
     * @throws ConfigException
65
     * @throws InvalidArgumentException
66
     * @throws ReflectionException
67
     */
68 1
    public function __construct()
69
    {
70 1
        $this->finder = new Finder();
71 1
        $this->cache = Cache::getInstance();
72 1
        $this->init();
73 1
    }
74
75
    /**
76
     * @throws GeneratorException
77
     * @throws ConfigException
78
     * @throws InvalidArgumentException
79
     * @throws ReflectionException
80
     */
81 2
    public function init()
82
    {
83 2
        list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', $this->cacheType, TRUE);
84 2
        $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->cacheType, TRUE);
85 2
        if (empty($this->routing) || Config::getParam('debug', true)) {
86 2
            $this->debugLoad();
87
        }
88 2
        $this->checkExternalModules(false);
89 2
        $this->setLoaded();
90 2
    }
91
92
    /**
93
     * @throws GeneratorException
94
     * @throws ConfigException
95
     * @throws InvalidArgumentException
96
     * @throws ReflectionException
97
     */
98 2
    private function debugLoad() {
99 2
        Logger::log('Begin routes load');
100 2
        $this->hydrateRouting();
101 2
        $this->simpatize();
102 2
        Logger::log('End routes load');
103 2
    }
104
105
    /**
106
     * @param Exception|NULL $exception
107
     * @param bool $isJson
108
     * @return string
109
     * @throws RouterException
110
     * @throws GeneratorException
111
     */
112
    public function httpNotFound(Exception $exception = NULL, $isJson = false)
113
    {
114
        Inspector::stats('[Router] Throw not found exception', Inspector::SCOPE_DEBUG);
115
        if (NULL === $exception) {
116
            Logger::log('Not found page thrown without previous exception', LOG_WARNING);
117
            $exception = new Exception(t('Page not found'), 404);
118
        }
119
        $template = Template::getInstance()->setStatus($exception->getCode());
120
        if ($isJson || false !== stripos(Request::getInstance()->getServer('CONTENT_TYPE'), 'json')) {
121
            $response = new JsonResponse(null, false, 0, 0, $exception->getMessage());
122
            return $template->output(json_encode($response), 'application/json');
123
        }
124
125
        $notFoundRoute = Config::getParam('route.404');
126
        if(null !== $notFoundRoute) {
127
            Request::getInstance()->redirect($this->getRoute($notFoundRoute, true));
128
        } else {
129
            return $template->render('error.html.twig', array(
130
                'exception' => $exception,
131
                'trace' => $exception->getTraceAsString(),
132
                'error_page' => TRUE,
133
            ));
134
        }
135
    }
136
137
    /**
138
     * @return array
139
     */
140 1
    public function getSlugs()
141
    {
142 1
        return $this->slugs;
143
    }
144
145
    /**
146
     * @return array
147
     */
148 2
    public function getRoutes() {
149 2
        return $this->routing;
150
    }
151
152
    /**
153
     * Method that extract all routes in the platform
154
     * @return array
155
     */
156 1
    public function getAllRoutes()
157
    {
158 1
        $routes = [];
159 1
        foreach ($this->getRoutes() as $path => $route) {
160 1
            if (array_key_exists('slug', $route)) {
161 1
                $routes[$route['slug']] = $path;
162
            }
163
        }
164 1
        return $routes;
165
    }
166
167
    /**
168
     * @param string|null $route
169
     *
170
     * @throws Exception
171
     * @return string HTML
172
     */
173 2
    public function execute($route)
174
    {
175 2
        Inspector::stats('[Router] Executing the request', Inspector::SCOPE_DEBUG);
176
        try {
177
            //Search action and execute
178 2
            return $this->searchAction($route);
179 1
        } catch (AccessDeniedException $e) {
180
            Logger::log(t('Solicitamos credenciales de acceso a zona restringida'), LOG_WARNING, ['file' => $e->getFile() . '[' . $e->getLine() . ']']);
181
            return Admin::staticAdminLogon($route);
182 1
        } catch (RouterException $r) {
183 1
            Logger::log($r->getMessage(), LOG_WARNING);
184
        } catch (Exception $e) {
185
            Logger::log($e->getMessage(), LOG_ERR);
186
            throw $e;
187
        }
188
189 1
        throw new RouterException(t('Página no encontrada'), 404);
190
    }
191
192
    /**
193
     * @param string $route
194
     * @return mixed
195
     * @throws AccessDeniedException
196
     * @throws AdminCredentialsException
197
     * @throws RouterException
198
     * @throws Exception
199
     */
200 2
    protected function searchAction($route)
201
    {
202 2
        Inspector::stats('[Router] Searching action to execute: ' . $route, Inspector::SCOPE_DEBUG);
203
        //Revisamos si tenemos la ruta registrada
204 2
        $parts = parse_url($route);
205 2
        $path = array_key_exists('path', $parts) ? $parts['path'] : $route;
206 2
        $httpRequest = Request::getInstance()->getMethod();
207 2
        foreach ($this->routing as $pattern => $action) {
208 2
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
209 2
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
210 2
            if ($matched && ($httpMethod === 'ALL' || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
211
                // Checks restricted access
212 1
                SecurityHelper::checkRestrictedAccess($route);
213 1
                $get = RouterHelper::extractComponents($route, $routePattern);
214
                /** @var $class Controller */
215 1
                $class = RouterHelper::getClassToCall($action);
216
                try {
217 1
                    if($this->checkRequirements($action, $get)) {
218 1
                        return $this->executeCachedRoute($route, $action, $class, $get);
0 ignored issues
show
Bug introduced by
$class of type PSFS\base\types\Controller is incompatible with the type string expected by parameter $class of PSFS\base\Router::executeCachedRoute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
                        return $this->executeCachedRoute($route, $action, /** @scrutinizer ignore-type */ $class, $get);
Loading history...
219
                    } else {
220
                        throw new RouterException(t('La ruta no es válida'), 400);
221
                    }
222
                } catch (Exception $e) {
223
                    Logger::log($e->getMessage(), LOG_ERR);
224 1
                    throw $e;
225
                }
226
            }
227
        }
228 1
        throw new RouterException(t('Ruta no encontrada'));
229
    }
230
231
    /**
232
     * @param array $action
233
     * @param array $params
234
     * @return bool
235
     */
236 1
    private function checkRequirements(array $action, $params = []) {
237 1
        Inspector::stats('[Router] Checking request requirements', Inspector::SCOPE_DEBUG);
238 1
        if(!empty($params) && !empty($action['requirements'])) {
239
            $checked = 0;
240
            foreach(array_keys($params) as $key) {
241
                if(in_array($key, $action['requirements'], true)) {
242
                    $checked++;
243
                }
244
            }
245
            $valid = count($action['requirements']) === $checked;
246
        } else {
247 1
            $valid = true;
248
        }
249 1
        return $valid;
250
    }
251
252
    /**
253
     * @return string|null
254
     */
255 2
    private function getExternalModules() {
256 2
        $externalModules = Config::getParam('modules.extend', '');
257 2
        $externalModules .= ',psfs/auth,psfs/nosql';
258 2
        return $externalModules;
259
    }
260
261
    /**
262
     * @param boolean $hydrateRoute
263
     */
264 2
    private function checkExternalModules($hydrateRoute = true)
265
    {
266 2
        $externalModules = $this->getExternalModules();
267 2
        $externalModules = explode(',', $externalModules);
268 2
        foreach ($externalModules as $module) {
269 2
            if(strlen($module)) {
270 2
                $this->loadExternalModule($hydrateRoute, $module);
271
            }
272
        }
273 2
    }
274
275
    /**
276
     * @throws ConfigException
277
     * @throws InvalidArgumentException
278
     * @throws ReflectionException
279
     * @throws GeneratorException
280
     */
281 2
    private function generateRouting()
282
    {
283 2
        $base = SOURCE_DIR;
284 2
        $modulesPath = realpath(CORE_DIR);
285 2
        $this->routing = $this->inspectDir($base, 'PSFS', array());
286 2
        $this->checkExternalModules();
287 2
        if (file_exists($modulesPath)) {
288 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
289 1
            if($modules->hasResults()) {
290 1
                foreach ($modules->getIterator() as $modulePath) {
291 1
                    $module = $modulePath->getBasename();
292 1
                    $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
293
                }
294
            }
295
        }
296 2
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->domains, Cache::JSON, TRUE);
297 2
    }
298
299
    /**
300
     * @throws GeneratorException
301
     * @throws ConfigException
302
     * @throws InvalidArgumentException
303
     * @throws ReflectionException
304
     */
305 2
    public function hydrateRouting()
306
    {
307 2
        $this->generateRouting();
308 2
        $home = Config::getParam('home.action', 'admin');
309 2
        if (NULL !== $home || $home !== '') {
310 2
            $homeParams = NULL;
311 2
            foreach ($this->routing as $pattern => $params) {
312 2
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
313 2
                if (preg_match('/' . preg_quote($route, '/') . '$/i', '/' . $home)) {
314 2
                    $homeParams = $params;
315
                }
316 2
                unset($method);
317
            }
318 2
            if (NULL !== $homeParams) {
319 2
                $this->routing['/'] = $homeParams;
320
            }
321
        }
322 2
    }
323
324
    /**
325
     * @param string $origen
326
     * @param string $namespace
327
     * @param array $routing
328
     * @return array
329
     * @throws ReflectionException
330
     * @throws ConfigException
331
     * @throws InvalidArgumentException
332
     */
333 2
    private function inspectDir($origen, $namespace = 'PSFS', $routing = [])
334
    {
335 2
        $files = $this->finder->files()->in($origen)->path('/(controller|api)/i')->depth('< 3')->name('*.php');
336 2
        if($files->hasResults()) {
337 2
            foreach ($files->getIterator() as $file) {
338 2
                if($namespace !== 'PSFS' && method_exists($file, 'getRelativePathname')) {
339 1
                    $filename = '\\' . str_replace('/', '\\', str_replace($origen, '', $file->getRelativePathname()));
340
                } else {
341 2
                    $filename = str_replace('/', '\\', str_replace($origen, '', $file->getPathname()));
342
                }
343 2
                $routing = $this->addRouting($namespace . str_replace('.php', '', $filename), $routing, $namespace);
344
            }
345
        }
346 2
        $this->finder = new Finder();
347
348 2
        return $routing;
349
    }
350
351
    /**
352
     * @param string $namespace
353
     * @return bool
354
     */
355 4
    public static function exists($namespace)
356
    {
357 4
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
358
    }
359
360
    /**
361
     * @param string $namespace
362
     * @param array $routing
363
     * @param string $module
364
     * @return array
365
     * @throws ReflectionException
366
     */
367 2
    private function addRouting($namespace, &$routing, $module = 'PSFS')
368
    {
369 2
        if (self::exists($namespace)) {
370 2
            if(I18nHelper::checkI18Class($namespace)) {
371
                return $routing;
372
            }
373 2
            $reflection = new ReflectionClass($namespace);
374 2
            if (false === $reflection->isAbstract() && FALSE === $reflection->isInterface()) {
375 2
                $this->extractDomain($reflection);
376 2
                $classComments = $reflection->getDocComment();
377 2
                $api = AnnotationHelper::extractApi($classComments);
378 2
                foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
379 2
                    $route = AnnotationHelper::extractRoute($method->getDocComment());
380 2
                    if (null !== $route) {
381 2
                        list($route, $info) = RouterHelper::extractRouteInfo($method, str_replace('\\', '', $api), str_replace('\\', '', $module));
382
383 2
                        if (null !== $route && null !== $info) {
384 2
                            $info['class'] = $namespace;
385 2
                            $routing[$route] = $info;
386
                        }
387
                    }
388
                }
389
            }
390
        }
391
392 2
        return $routing;
393
    }
394
395
    /**
396
     *
397
     * @param ReflectionClass $class
398
     *
399
     * @return Router
400
     * @throws ConfigException
401
     */
402 2
    protected function extractDomain(ReflectionClass $class)
403
    {
404
        //Calculamos los dominios para las plantillas
405 2
        if ($class->hasConstant('DOMAIN') && !$class->isAbstract()) {
406 2
            if (!$this->domains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->domains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
407 2
                $this->domains = [];
408
            }
409 2
            $domain = '@' . $class->getConstant('DOMAIN') . '/';
410 2
            if (!array_key_exists($domain, $this->domains)) {
411 2
                $this->domains[$domain] = RouterHelper::extractDomainInfo($class, $domain);
412
            }
413
        }
414
415 2
        return $this;
416
    }
417
418
    /**
419
     * @return $this
420
     * @throws GeneratorException
421
     */
422 2
    public function simpatize()
423
    {
424 2
        $this->generateSlugs();
425 2
        GeneratorHelper::createDir(CONFIG_DIR);
426 2
        Cache::getInstance()->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', array($this->routing, $this->slugs), Cache::JSON, TRUE);
427
428 2
        return $this;
429
    }
430
431
    /**
432
     * @param string $slug
433
     * @param boolean $absolute
434
     * @param array $params
435
     *
436
     * @return string|null
437
     * @throws RouterException
438
     */
439 3
    public function getRoute($slug = '', $absolute = false, array $params = [])
440
    {
441 3
        $baseUrl = $absolute ? Request::getInstance()->getRootUrl() : '';
442 3
        if ('' === $slug) {
443 1
            return $baseUrl . '/';
444
        }
445 3
        if (!is_array($this->slugs) || !array_key_exists($slug, $this->slugs)) {
0 ignored issues
show
introduced by
The condition is_array($this->slugs) is always true.
Loading history...
446 1
            throw new RouterException(t('No existe la ruta especificada'));
447
        }
448 3
        $url = $baseUrl . $this->slugs[$slug];
449 3
        if (!empty($params)) {
450
            foreach ($params as $key => $value) {
451
                $url = str_replace('{' . $key . '}', $value, $url);
452
            }
453 3
        } elseif (!empty($this->routing[$this->slugs[$slug]]['default'])) {
454 3
            $url = $baseUrl . $this->routing[$this->slugs[$slug]]['default'];
455
        }
456
457 3
        return preg_replace('/(GET|POST|PUT|DELETE|ALL|HEAD|PATCH)\#\|\#/', '', $url);
458
    }
459
460
    /**
461
     * @return array
462
     */
463 3
    public function getDomains()
464
    {
465 3
        return $this->domains ?: [];
466
    }
467
468
    /**
469
     * @param string $class
470
     * @param string $method
471
     */
472 1
    private function checkPreActions($class, $method) {
473 1
        $preAction = 'pre' . ucfirst($method);
474 1
        if(method_exists($class, $preAction)) {
475
            Inspector::stats('[Router] Pre action invoked', Inspector::SCOPE_DEBUG);
476
            try {
477
                if(false === call_user_func_array([$class, $preAction])) {
0 ignored issues
show
Bug introduced by
The call to call_user_func_array() has too few arguments starting with param_arr. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

477
                if(false === /** @scrutinizer ignore-call */ call_user_func_array([$class, $preAction])) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
478
                    Logger::log(t('Pre action failed'), LOG_ERR, [error_get_last()]);
479
                    error_clear_last();
480
                }
481
            } catch (Exception $e) {
482
                Logger::log($e->getMessage(), LOG_ERR, [$class, $method]);
483
            }
484
        }
485 1
    }
486
487
    /**
488
     * @param string $route
489
     * @param array $action
490
     * @param string $class
491
     * @param array $params
492
     * @return mixed
493
     * @throws exception\GeneratorException
494
     * @throws ConfigException
495
     */
496 1
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
497
    {
498 1
        Inspector::stats('[Router] Executing route ' . $route, Inspector::SCOPE_DEBUG);
499 1
        $action['params'] = array_merge($action['params'], $params, Request::getInstance()->getQueryParams());
500 1
        Security::getInstance()->setSessionKey(Cache::CACHE_SESSION_VAR, $action);
501 1
        $cache = Cache::needCache();
502 1
        $execute = TRUE;
503 1
        $return = null;
504 1
        if (FALSE !== $cache && $action['http'] === 'GET' && Config::getParam('debug') === FALSE) {
505
            list($path, $cacheDataName) = $this->cache->getRequestCacheHash();
506
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, $cache);
0 ignored issues
show
Bug introduced by
It seems like $cache can also be of type true; however, parameter $expires of PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

506
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, /** @scrutinizer ignore-type */ $cache);
Loading history...
507
            if (NULL !== $cachedData) {
508
                $headers = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName . '.headers', $cache, null, Cache::JSON);
509
                Template::getInstance()->renderCache($cachedData, $headers);
510
                $execute = FALSE;
511
            }
512
        }
513 1
        if ($execute) {
514 1
            Inspector::stats('[Router] Start executing action ' . $route, Inspector::SCOPE_DEBUG);
515 1
            $this->checkPreActions($class, $action['method']);
516 1
            $return = call_user_func_array([$class, $action['method']], $params);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $param_arr of call_user_func_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

516
            $return = call_user_func_array([$class, $action['method']], /** @scrutinizer ignore-type */ $params);
Loading history...
517 1
            if (false === $return) {
518
                Logger::log(t('An error occurred trying to execute the action'), LOG_ERR, [error_get_last()]);
519
            }
520
        }
521 1
        return $return;
522
    }
523
524
    /**
525
     * Parse slugs to create translations
526
     */
527 2
    private function generateSlugs()
528
    {
529 2
        foreach ($this->routing as $key => &$info) {
530 2
            $keyParts = explode('#|#', $key);
531 2
            $keyParts = array_key_exists(1, $keyParts) ? $keyParts[1] : $keyParts[0];
532 2
            $slug = RouterHelper::slugify($keyParts);
533 2
            $this->slugs[$slug] = $key;
534 2
            $info['slug'] = $slug;
535
            // TODO add routes to translations JSON
536
        }
537 2
    }
538
539
    /**
540
     * @param boolean $hydrateRoute
541
     * @param SplFileInfo $modulePath
542
     * @param string $externalModulePath
543
     * @throws ReflectionException
544
     */
545
    private function loadExternalAutoloader($hydrateRoute, SplFileInfo $modulePath, $externalModulePath)
546
    {
547
        $extModule = $modulePath->getBasename();
548
        $moduleAutoloader = realpath($externalModulePath . DIRECTORY_SEPARATOR . $extModule . DIRECTORY_SEPARATOR . 'autoload.php');
549
        if(file_exists($moduleAutoloader)) {
550
            include_once $moduleAutoloader;
551
            if ($hydrateRoute) {
552
                $this->routing = $this->inspectDir($externalModulePath . DIRECTORY_SEPARATOR . $extModule, '\\' . $extModule, $this->routing);
553
            }
554
        }
555
    }
556
557
    /**
558
     * @param $hydrateRoute
559
     * @param $module
560
     * @return mixed
561
     */
562 2
    private function loadExternalModule($hydrateRoute, $module)
563
    {
564
        try {
565 2
            $module = preg_replace('/(\\\|\/)/', DIRECTORY_SEPARATOR, $module);
566 2
            $externalModulePath = VENDOR_DIR . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'src';
567 2
            if(file_exists($externalModulePath)) {
568
                $externalModule = $this->finder->directories()->in($externalModulePath)->depth(0);
569
                if($externalModule->hasResults()) {
570
                    foreach ($externalModule->getIterator() as $modulePath) {
571 2
                        $this->loadExternalAutoloader($hydrateRoute, $modulePath, $externalModulePath);
572
                    }
573
                }
574
            }
575
        } catch (Exception $e) {
576
            Logger::log($e->getMessage(), LOG_WARNING);
577
        }
578 2
    }
579
580
}
581