Test Failed
Push — master ( 79af4d...00f9a5 )
by Fran
02:26
created

Router::checkRequirements()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 10
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 6
rs 9.2222
1
<?php
2
3
namespace PSFS\base;
4
5
use Exception;
6
use InvalidArgumentException;
7
use PSFS\base\config\Config;
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\GeneratorHelper;
15
use PSFS\base\types\helpers\Inspector;
16
use PSFS\base\types\helpers\ResponseHelper;
17
use PSFS\base\types\helpers\RouterHelper;
18
use PSFS\base\types\helpers\SecurityHelper;
19
use PSFS\base\types\traits\Router\ModulesTrait;
20
use PSFS\base\types\traits\SingletonTrait;
21
use PSFS\controller\base\Admin;
22
use ReflectionException;
23
24
/**
25
 * Class Router
26
 * @package PSFS
27
 */
28
class Router
29
{
30
    use SingletonTrait;
31
    use ModulesTrait;
32
33
    const PSFS_BASE_NAMESPACE = 'PSFS';
34
35
    /**
36
     * @var array
37
     */
38
    private $routing = [];
39
    /**
40
     * @var array
41
     */
42
    private $slugs = [];
43
    /**
44
     * @var Cache $cache
45
     */
46
    private $cache;
47
    /**
48
     * @var int
49
     */
50
    protected $cacheType = Cache::JSON;
51
52
    /**
53
     * Router constructor.
54
     * @throws GeneratorException
55
     * @throws ConfigException
56
     * @throws InvalidArgumentException
57
     * @throws ReflectionException
58
     */
59 2
    public function __construct()
60
    {
61 2
        $this->cache = Cache::getInstance();
62 2
        $this->initializeFinder();
63 2
        $this->init();
64 2
    }
65
66
    /**
67
     * @throws GeneratorException
68
     * @throws ConfigException
69
     * @throws InvalidArgumentException
70
     * @throws ReflectionException
71
     */
72 3
    public function init()
73
    {
74 3
        list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', $this->cacheType, TRUE);
75 3
        $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->cacheType, TRUE);
76 3
        if (empty($this->routing) || Config::getParam('debug', true)) {
77 3
            $this->debugLoad();
78
        }
79 3
        $this->checkExternalModules(false);
80 3
        $this->setLoaded();
81 3
    }
82
83
    /**
84
     * @throws GeneratorException
85
     * @throws ConfigException
86
     * @throws InvalidArgumentException
87
     * @throws ReflectionException
88
     */
89 3
    private function debugLoad()
90
    {
91 3
        Logger::log('Begin routes load');
92 3
        $this->hydrateRouting();
93 3
        $this->simpatize();
94 3
        Logger::log('End routes load');
95 3
    }
96
97
    /**
98
     * @return array
99
     */
100 1
    public function getSlugs()
101
    {
102 1
        return $this->slugs;
103
    }
104
105
    /**
106
     * @return array
107
     */
108 2
    public function getRoutes()
109
    {
110 2
        return $this->routing;
111
    }
112
113
    /**
114
     * Method that extract all routes in the platform
115
     * @return array
116
     */
117 1
    public function getAllRoutes()
118
    {
119 1
        $routes = [];
120 1
        foreach ($this->getRoutes() as $path => $route) {
121 1
            if (array_key_exists('slug', $route)) {
122 1
                $routes[$route['slug']] = $path;
123
            }
124
        }
125 1
        return $routes;
126
    }
127
128
    /**
129
     * @param string|null $route
130
     *
131
     * @return string HTML
132
     * @throws Exception
133
     */
134 5
    public function execute($route)
135
    {
136 5
        Inspector::stats('[Router] Executing the request', Inspector::SCOPE_DEBUG);
137 5
        $code = 404;
0 ignored issues
show
Unused Code introduced by
The assignment to $code is dead and can be removed.
Loading history...
138
        try {
139
            //Search action and execute
140 5
            return $this->searchAction($route);
141 4
        } catch (AccessDeniedException $e) {
142
            Logger::log(t('Solicitamos credenciales de acceso a zona restringida'), LOG_WARNING, ['file' => $e->getFile() . '[' . $e->getLine() . ']']);
143
            return Admin::staticAdminLogon();
144 4
        } catch (RouterException $r) {
145 3
            Logger::log($r->getMessage(), LOG_WARNING);
146 3
            $code = $r->getCode();
147 1
        } catch (Exception $e) {
148 1
            Logger::log($e->getMessage(), LOG_ERR);
149 1
            throw $e;
150
        }
151
152 3
        throw new RouterException(t('Página no encontrada'), $code);
153
    }
154
155
    /**
156
     * @param string $route
157
     * @return mixed
158
     * @throws AccessDeniedException
159
     * @throws AdminCredentialsException
160
     * @throws RouterException
161
     * @throws Exception
162
     */
163 5
    protected function searchAction($route)
164
    {
165 5
        Inspector::stats('[Router] Searching action to execute: ' . $route, Inspector::SCOPE_DEBUG);
166
        //Revisamos si tenemos la ruta registrada
167 5
        $parts = parse_url($route);
168 5
        $path = array_key_exists('path', $parts) ? $parts['path'] : $route;
169 5
        $httpRequest = Request::getInstance()->getMethod();
170 5
        foreach ($this->routing as $pattern => $action) {
171 5
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
172 5
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
173 5
            if ($matched && ($httpMethod === 'ALL' || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
174
                // Checks restricted access
175 4
                SecurityHelper::checkRestrictedAccess($route);
176 3
                $get = RouterHelper::extractComponents($route, $routePattern);
177
                /** @var $class Controller */
178 3
                $class = RouterHelper::getClassToCall($action);
179
                try {
180 3
                    if ($this->checkRequirements($action, $get)) {
181 2
                        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

181
                        return $this->executeCachedRoute($route, $action, /** @scrutinizer ignore-type */ $class, $get);
Loading history...
182
                    } else {
183 1
                        throw new RouterException(t('Preconditions failed'), 412);
184
                    }
185 2
                } catch (Exception $e) {
186 2
                    Logger::log($e->getMessage(), LOG_ERR);
187 5
                    throw $e;
188
                }
189
            }
190
        }
191 1
        throw new RouterException(t('Ruta no encontrada'));
192
    }
193
194
    /**
195
     * @param array $action
196
     * @param array $params
197
     * @return bool
198
     */
199 3
    private function checkRequirements(array $action, $params = [])
200
    {
201 3
        Inspector::stats('[Router] Checking request requirements', Inspector::SCOPE_DEBUG);
202 3
        if (!empty($params) && !empty($action['requirements'])) {
203 2
            $checked = 0;
204 2
            foreach (array_keys($params) as $key) {
205 2
                if (in_array($key, $action['requirements'], true) && !empty($params[$key])) {
206 2
                    $checked++;
207
                }
208
            }
209 2
            $valid = count($action['requirements']) === $checked;
210
        } else {
211 1
            $valid = true;
212
        }
213 3
        return $valid;
214
    }
215
216
    /**
217
     * @return string|null
218
     */
219 3
    private function getExternalModules()
220
    {
221 3
        $externalModules = Config::getParam('modules.extend', '');
222 3
        $externalModules .= ',psfs/auth,psfs/nosql';
223 3
        return $externalModules;
224
    }
225
226
    /**
227
     * @param boolean $hydrateRoute
228
     */
229 3
    private function checkExternalModules($hydrateRoute = true)
230
    {
231 3
        $externalModules = $this->getExternalModules();
232 3
        $externalModules = explode(',', $externalModules);
233 3
        foreach ($externalModules as $module) {
234 3
            if (strlen($module)) {
235 3
                $this->loadExternalModule($hydrateRoute, $module, $this->routing);
236
            }
237
        }
238 3
    }
239
240
    /**
241
     * @throws ConfigException
242
     * @throws InvalidArgumentException
243
     * @throws ReflectionException
244
     * @throws GeneratorException
245
     */
246 3
    private function generateRouting()
247
    {
248 3
        $base = SOURCE_DIR;
249 3
        $modulesPath = realpath(CORE_DIR);
250 3
        $this->routing = $this->inspectDir($base, 'PSFS', array());
251 3
        $this->checkExternalModules();
252 3
        if (file_exists($modulesPath)) {
253 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
254 1
            if ($modules->hasResults()) {
255 1
                foreach ($modules->getIterator() as $modulePath) {
256 1
                    $module = $modulePath->getBasename();
257 1
                    $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
258
                }
259
            }
260
        }
261 3
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->domains, Cache::JSON, TRUE);
262 3
    }
263
264
    /**
265
     * @throws GeneratorException
266
     * @throws ConfigException
267
     * @throws InvalidArgumentException
268
     * @throws ReflectionException
269
     */
270 3
    public function hydrateRouting()
271
    {
272 3
        $this->generateRouting();
273 3
        $home = Config::getParam('home.action', 'admin');
274 3
        if (NULL !== $home || $home !== '') {
275 3
            $homeParams = NULL;
276 3
            foreach ($this->routing as $pattern => $params) {
277 3
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
278 3
                if (preg_match('/' . preg_quote($route, '/') . '$/i', '/' . $home)) {
279 3
                    $homeParams = $params;
280
                }
281 3
                unset($method);
282
            }
283 3
            if (NULL !== $homeParams) {
284 3
                $this->routing['/'] = $homeParams;
285
            }
286
        }
287 3
    }
288
289
    /**
290
     * @param string $namespace
291
     * @return bool
292
     */
293 5
    public static function exists($namespace)
294
    {
295 5
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
296
    }
297
298
    /**
299
     * @return $this
300
     * @throws GeneratorException
301
     */
302 3
    public function simpatize()
303
    {
304 3
        $this->generateSlugs();
305 3
        GeneratorHelper::createDir(CONFIG_DIR);
306 3
        Cache::getInstance()->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', array($this->routing, $this->slugs), Cache::JSON, TRUE);
307
308 3
        return $this;
309
    }
310
311
    /**
312
     * @param string $slug
313
     * @param boolean $absolute
314
     * @param array $params
315
     *
316
     * @return string|null
317
     * @throws RouterException
318
     */
319 3
    public function getRoute($slug = '', $absolute = false, array $params = [])
320
    {
321 3
        $baseUrl = $absolute ? Request::getInstance()->getRootUrl() : '';
322 3
        if ('' === $slug) {
323 1
            return $baseUrl . '/';
324
        }
325 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...
326 1
            throw new RouterException(t('No existe la ruta especificada'));
327
        }
328 3
        $url = $baseUrl . $this->slugs[$slug];
329 3
        if (!empty($params)) {
330
            foreach ($params as $key => $value) {
331
                $url = str_replace('{' . $key . '}', $value, $url);
332
            }
333 3
        } elseif (!empty($this->routing[$this->slugs[$slug]]['default'])) {
334 3
            $url = $baseUrl . $this->routing[$this->slugs[$slug]]['default'];
335
        }
336
337 3
        return preg_replace('/(GET|POST|PUT|DELETE|ALL|HEAD|PATCH)\#\|\#/', '', $url);
338
    }
339
340
    /**
341
     * @param string $class
342
     * @param string $method
343
     */
344 2
    private function checkPreActions($class, $method)
345
    {
346 2
        $preAction = 'pre' . ucfirst($method);
347 2
        if (method_exists($class, $preAction)) {
348
            Inspector::stats('[Router] Pre action invoked', Inspector::SCOPE_DEBUG);
349
            try {
350
                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

350
                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...
351
                    Logger::log(t('Pre action failed'), LOG_ERR, [error_get_last()]);
352
                    error_clear_last();
353
                }
354
            } catch (Exception $e) {
355
                Logger::log($e->getMessage(), LOG_ERR, [$class, $method]);
356
            }
357
        }
358 2
    }
359
360
    /**
361
     * @param string $route
362
     * @param array $action
363
     * @param string $class
364
     * @param array $params
365
     * @return mixed
366
     * @throws GeneratorException
367
     * @throws ConfigException
368
     */
369 2
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
370
    {
371 2
        Inspector::stats('[Router] Executing route ' . $route, Inspector::SCOPE_DEBUG);
372 2
        $action['params'] = array_merge($action['params'], $params, Request::getInstance()->getQueryParams());
373 2
        Security::getInstance()->setSessionKey(Cache::CACHE_SESSION_VAR, $action);
374 2
        $cache = Cache::needCache();
375 2
        $execute = TRUE;
376 2
        $return = null;
377 2
        if (FALSE !== $cache && $action['http'] === 'GET' && Config::getParam('debug') === FALSE) {
378
            list($path, $cacheDataName) = $this->cache->getRequestCacheHash();
379
            $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

379
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, /** @scrutinizer ignore-type */ $cache);
Loading history...
380
            if (NULL !== $cachedData) {
381
                $headers = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName . '.headers', $cache, null, Cache::JSON);
382
                Template::getInstance()->renderCache($cachedData, $headers);
383
                $execute = FALSE;
384
            }
385
        }
386 2
        if ($execute) {
387 2
            Inspector::stats('[Router] Start executing action ' . $route, Inspector::SCOPE_DEBUG);
388 2
            $this->checkPreActions($class, $action['method']);
389 2
            $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

389
            $return = call_user_func_array([$class, $action['method']], /** @scrutinizer ignore-type */ $params);
Loading history...
390 1
            if (false === $return) {
391
                Logger::log(t('An error occurred trying to execute the action'), LOG_ERR, [error_get_last()]);
392
            }
393
        }
394 1
        return $return;
395
    }
396
397
    /**
398
     * Parse slugs to create translations
399
     */
400 3
    private function generateSlugs()
401
    {
402 3
        foreach ($this->routing as $key => &$info) {
403 3
            $keyParts = explode('#|#', $key);
404 3
            $keyParts = array_key_exists(1, $keyParts) ? $keyParts[1] : $keyParts[0];
405 3
            $slug = RouterHelper::slugify($keyParts);
406 3
            $this->slugs[$slug] = $key;
407 3
            $info['slug'] = $slug;
408
            // TODO add routes to translations JSON
409
        }
410 3
    }
411
412
    /**
413
     * @param Exception|null $exception
414
     * @param bool $isJson
415
     * @return string
416
     * @throws GeneratorException
417
     */
418
    public function httpNotFound(\Exception $exception = null, $isJson = false)
419
    {
420
        return ResponseHelper::httpNotFound($exception, $isJson);
421
    }
422
}
423