Passed
Push — master ( 7d2d22...482c1b )
by Fran
03:18
created

Router::hydrateRouting()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 7
nop 0
dl 0
loc 15
ccs 12
cts 12
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0
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\Inspector;
15
use PSFS\base\types\helpers\ResponseHelper;
16
use PSFS\base\types\helpers\RouterHelper;
17
use PSFS\base\types\helpers\SecurityHelper;
18
use PSFS\base\types\interfaces\PreConditionedRunInterface;
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
     * @var Cache $cache
36
     */
37
    private $cache;
38
    /**
39
     * @var int
40
     */
41
    protected $cacheType = Cache::JSON;
42
43
    /**
44
     * Router constructor.
45
     * @throws GeneratorException
46
     * @throws ConfigException
47
     * @throws InvalidArgumentException
48
     * @throws ReflectionException
49
     */
50 4
    public function __construct()
51
    {
52 4
        $this->cache = Cache::getInstance();
53 4
        $this->initializeFinder();
54 4
        $this->init();
55
    }
56
57
    /**
58
     * @throws GeneratorException
59
     * @throws ConfigException
60
     * @throws InvalidArgumentException
61
     * @throws ReflectionException
62
     */
63 5
    public function init()
64
    {
65 5
        list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', $this->cacheType, TRUE);
66 5
        $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->cacheType, TRUE);
67 5
        if (empty($this->routing) || Config::getParam('debug', true)) {
68 5
            $this->debugLoad();
69
        }
70 5
        $this->checkExternalModules(false);
71 5
        $this->setLoaded();
72
    }
73
74
    /**
75
     * @throws GeneratorException
76
     * @throws ConfigException
77
     * @throws InvalidArgumentException
78
     * @throws ReflectionException
79
     */
80 5
    private function debugLoad()
81
    {
82 5
        if (!Config::getParam('skip.route_generation', false)) {
83 5
            Logger::log('Begin routes load');
84 5
            $this->hydrateRouting();
85 5
            $this->simpatize();
86 5
            Logger::log('End routes load');
87
        } else {
88
            Logger::log('Routes generation skipped');
89
        }
90
    }
91
92
    /**
93
     * @param string|null $route
94
     *
95
     * @return string HTML
96
     * @throws Exception
97
     */
98 5
    public function execute($route)
99
    {
100 5
        Inspector::stats('[Router] Executing the request', Inspector::SCOPE_DEBUG);
101
        try {
102
            //Search action and execute
103 5
            return $this->searchAction($route);
104 4
        } catch (AccessDeniedException $e) {
105 2
            Logger::log(t('Solicitamos credenciales de acceso a zona restringida'), LOG_WARNING, ['file' => $e->getFile() . '[' . $e->getLine() . ']']);
106 2
            return Admin::staticAdminLogon();
107 2
        } catch (RouterException $r) {
108 2
            Logger::log($r->getMessage(), LOG_WARNING);
109 2
            $code = $r->getCode();
110
        } catch (Exception $e) {
111
            Logger::log($e->getMessage(), LOG_ERR);
112
            throw $e;
113
        }
114
115 2
        throw new RouterException(t('Página no encontrada'), $code);
116
    }
117
118
    /**
119
     * @param string $route
120
     * @return mixed
121
     * @throws AccessDeniedException
122
     * @throws AdminCredentialsException
123
     * @throws RouterException
124
     * @throws Exception
125
     */
126 5
    protected function searchAction($route)
127
    {
128 5
        Inspector::stats('[Router] Searching action to execute: ' . $route, Inspector::SCOPE_DEBUG);
129
        //Revisamos si tenemos la ruta registrada
130 5
        $parts = parse_url($route);
131 5
        $path = array_key_exists('path', $parts) ? $parts['path'] : $route;
132 5
        $httpRequest = Request::getInstance()->getMethod();
133 5
        foreach ($this->routing as $pattern => $action) {
134 5
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
135 5
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
136 5
            if ($matched && ($httpMethod === 'ALL' || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
137
                // Checks restricted access
138 3
                SecurityHelper::checkRestrictedAccess($route);
139 1
                $get = RouterHelper::extractComponents($route, $routePattern);
140
                /** @var $class Controller */
141 1
                $class = RouterHelper::getClassToCall($action);
142
                try {
143 1
                    if ($this->checkRequirements($action, $get)) {
144 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

144
                        return $this->executeCachedRoute($route, $action, /** @scrutinizer ignore-type */ $class, $get);
Loading history...
145
                    }
146
147
                    throw new RouterException(t('Preconditions failed'), 412);
148
                } catch (Exception $e) {
149
                    Logger::log($e->getMessage(), LOG_ERR);
150
                    throw $e;
151
                }
152
            }
153
        }
154 2
        throw new RouterException(t('Ruta no encontrada'));
155
    }
156
157
    /**
158
     * @param array $action
159
     * @param array $params
160
     * @return bool
161
     */
162 1
    private function checkRequirements(array $action, $params = [])
163
    {
164 1
        Inspector::stats('[Router] Checking request requirements', Inspector::SCOPE_DEBUG);
165 1
        if (!empty($params) && !empty($action['requirements'])) {
166
            $checked = 0;
167
            foreach (array_keys($params) as $key) {
168
                if (in_array($key, $action['requirements'], true) && strlen($params[$key])) {
169
                    $checked++;
170
                }
171
            }
172
            $valid = count($action['requirements']) === $checked;
173
        } else {
174 1
            $valid = true;
175
        }
176 1
        return $valid;
177
    }
178
179
    /**
180
     * @throws ConfigException
181
     * @throws InvalidArgumentException
182
     * @throws ReflectionException
183
     * @throws GeneratorException
184
     */
185 5
    private function generateRouting()
186
    {
187 5
        $base = SOURCE_DIR;
188 5
        $modulesPath = realpath(CORE_DIR);
189 5
        $this->routing = $this->inspectDir($base, 'PSFS', array());
190 5
        $this->checkExternalModules();
191 5
        if (file_exists($modulesPath)) {
192 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
193 1
            if ($modules->hasResults()) {
194 1
                foreach ($modules->getIterator() as $modulePath) {
195 1
                    $module = $modulePath->getBasename();
196 1
                    $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
197
                }
198
            }
199
        }
200 5
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->domains, Cache::JSON, TRUE);
201
    }
202
203
    /**
204
     * @throws GeneratorException
205
     * @throws ConfigException
206
     * @throws InvalidArgumentException
207
     * @throws ReflectionException
208
     */
209 5
    public function hydrateRouting()
210
    {
211 5
        $this->generateRouting();
212 5
        $home = Config::getParam('home.action', 'admin');
213 5
        if (NULL !== $home || $home !== '') {
214 5
            $homeParams = NULL;
215 5
            foreach ($this->routing as $pattern => $params) {
216 5
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
217 5
                if (preg_match('/' . preg_quote($route, '/') . '$/i', '/' . $home)) {
218 5
                    $homeParams = $params;
219
                }
220 5
                unset($method);
221
            }
222 5
            if (NULL !== $homeParams) {
223 5
                $this->routing['/'] = $homeParams;
224
            }
225
        }
226
    }
227
228
    /**
229
     * @param string $namespace
230
     * @return bool
231
     */
232 7
    public static function exists($namespace)
233
    {
234 7
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
235
    }
236
237
    /**
238
     * @param string $slug
239
     * @param boolean $absolute
240
     * @param array $params
241
     *
242
     * @return string|null
243
     * @throws RouterException
244
     */
245 2
    public function getRoute($slug = '', $absolute = false, array $params = [])
246
    {
247 2
        $baseUrl = $absolute ? Request::getInstance()->getRootUrl() : '';
248 2
        if ('' === $slug) {
249 1
            return $baseUrl . '/';
250
        }
251 2
        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...
252 1
            throw new RouterException(t('No existe la ruta especificada'));
253
        }
254 2
        $url = $baseUrl . $this->slugs[$slug];
255 2
        if (!empty($params)) {
256 1
            foreach ($params as $key => $value) {
257 1
                $url = str_replace('{' . $key . '}', $value, $url);
258
            }
259 2
        } elseif (!empty($this->routing[$this->slugs[$slug]]['default'])) {
260 2
            $url = $baseUrl . $this->routing[$this->slugs[$slug]]['default'];
261
        }
262
263 2
        return preg_replace('/(GET|POST|PUT|DELETE|ALL|HEAD|PATCH)\#\|\#/', '', $url);
264
    }
265
266
    /**
267
     * @param string $class
268
     * @param string $method
269
     */
270 1
    private function checkPreActions($class, $method)
271
    {
272 1
        if ($this->hasToRunPreChecks($class)) {
273
            self::run($class, '__check', true);
274
        }
275 1
        $preAction = 'pre' . ucfirst($method);
276 1
        if (method_exists($class, $preAction)) {
277
            self::run($class, $preAction);
278
        }
279
    }
280
281
    /**
282
     * @param $class
283
     * @param string $method
284
     * @param boolean $throwExceptions
285
     * @return void
286
     * @throws Exception
287
     */
288
    public static function run($class, $method, $throwExceptions = false): void
289
    {
290
        Inspector::stats("[Router] Pre action invoked " . get_class($class) . "::{$method}", Inspector::SCOPE_DEBUG);
291
        try {
292
            if (false === call_user_func_array([$class, $method], [])) {
293
                Logger::log(t("[Router] action " . get_class($class) . "::{$method} failed"), LOG_ERR, [error_get_last()]);
294
                error_clear_last();
295
            }
296
        } catch (Exception $e) {
297
            Logger::log($e->getMessage(), LOG_ERR, [$class, $method]);
298
            if ($throwExceptions) {
299
                throw $e;
300
            }
301
        }
302
    }
303
304
    /**
305
     * Check if class to run route implements the PreConditionedRunInterface
306
     * @param string $class
307
     * @return bool
308
     */
309 1
    private function hasToRunPreChecks($class)
310
    {
311 1
        return in_array(PreConditionedRunInterface::class, class_implements($class));
312
    }
313
314
    /**
315
     * @param string $route
316
     * @param array $action
317
     * @param string $class
318
     * @param array $params
319
     * @return mixed
320
     * @throws GeneratorException
321
     * @throws ConfigException
322
     */
323 1
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
324
    {
325 1
        Inspector::stats('[Router] Executing route ' . $route, Inspector::SCOPE_DEBUG);
326 1
        $action['params'] = array_merge($action['params'], $params, Request::getInstance()->getQueryParams());
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $arrays of array_merge() 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

326
        $action['params'] = array_merge($action['params'], /** @scrutinizer ignore-type */ $params, Request::getInstance()->getQueryParams());
Loading history...
327 1
        Security::getInstance()->setSessionKey(Cache::CACHE_SESSION_VAR, $action);
328 1
        $cache = Cache::needCache();
329 1
        $execute = TRUE;
330 1
        $return = null;
331 1
        if (FALSE !== $cache && $action['http'] === 'GET' && Config::getParam('debug') === FALSE) {
332
            list($path, $cacheDataName) = $this->cache->getRequestCacheHash();
333
            $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

333
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, /** @scrutinizer ignore-type */ $cache);
Loading history...
334
            if (NULL !== $cachedData) {
335
                $headers = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName . '.headers', $cache, null, Cache::JSON);
336
                Template::getInstance()->renderCache($cachedData, $headers);
337
                $execute = FALSE;
338
            }
339
        }
340 1
        if ($execute) {
341 1
            Inspector::stats('[Router] Start executing action ' . $route, Inspector::SCOPE_DEBUG);
342 1
            $this->checkPreActions($class, $action['method']);
343 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 $args 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

343
            $return = call_user_func_array([$class, $action['method']], /** @scrutinizer ignore-type */ $params);
Loading history...
344 1
            if (false === $return) {
345
                Logger::log(t('An error occurred trying to execute the action'), LOG_ERR, [error_get_last()]);
346
            }
347
        }
348 1
        return $return;
349
    }
350
351
    /**
352
     * @param Exception|null $exception
353
     * @param bool $isJson
354
     * @return string
355
     * @throws GeneratorException
356
     */
357
    public function httpNotFound(\Exception $exception = null, $isJson = false)
358
    {
359
        return ResponseHelper::httpNotFound($exception, $isJson);
360
    }
361
}
362