Passed
Push — master ( 786715...5dce3b )
by Fran
02:39
created

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

143
                        return $this->executeCachedRoute($route, $action, /** @scrutinizer ignore-type */ $class, $get);
Loading history...
144
                    }
145
146
                    throw new RouterException(t('Preconditions failed'), 412);
147
                } catch (Exception $e) {
148
                    Logger::log($e->getMessage(), LOG_ERR);
149
                    throw $e;
150
                }
151
            }
152
        }
153 2
        throw new RouterException(t('Ruta no encontrada'));
154
    }
155
156
    /**
157
     * @param array $action
158
     * @param array $params
159
     * @return bool
160
     */
161 1
    private function checkRequirements(array $action, $params = [])
162
    {
163 1
        Inspector::stats('[Router] Checking request requirements', Inspector::SCOPE_DEBUG);
164 1
        if (!empty($params) && !empty($action['requirements'])) {
165
            $checked = 0;
166
            foreach (array_keys($params) as $key) {
167
                if (in_array($key, $action['requirements'], true) && strlen($params[$key])) {
168
                    $checked++;
169
                }
170
            }
171
            $valid = count($action['requirements']) === $checked;
172
        } else {
173 1
            $valid = true;
174
        }
175 1
        return $valid;
176
    }
177
178
    /**
179
     * @throws ConfigException
180
     * @throws InvalidArgumentException
181
     * @throws ReflectionException
182
     * @throws GeneratorException
183
     */
184 5
    private function generateRouting()
185
    {
186 5
        $base = SOURCE_DIR;
187 5
        $modulesPath = realpath(CORE_DIR);
188 5
        $this->routing = $this->inspectDir($base, 'PSFS', array());
189 5
        $this->checkExternalModules();
190 5
        if (file_exists($modulesPath)) {
191 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
192 1
            if ($modules->hasResults()) {
193 1
                foreach ($modules->getIterator() as $modulePath) {
194 1
                    $module = $modulePath->getBasename();
195 1
                    $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
196
                }
197
            }
198
        }
199 5
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->domains, Cache::JSON, TRUE);
200
    }
201
202
    /**
203
     * @throws GeneratorException
204
     * @throws ConfigException
205
     * @throws InvalidArgumentException
206
     * @throws ReflectionException
207
     */
208 5
    public function hydrateRouting()
209
    {
210 5
        $this->generateRouting();
211 5
        $home = Config::getParam('home.action', 'admin');
212 5
        if (NULL !== $home || $home !== '') {
213 5
            $homeParams = NULL;
214 5
            foreach ($this->routing as $pattern => $params) {
215 5
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
216 5
                if (preg_match('/' . preg_quote($route, '/') . '$/i', '/' . $home)) {
217 5
                    $homeParams = $params;
218
                }
219 5
                unset($method);
220
            }
221 5
            if (NULL !== $homeParams) {
222 5
                $this->routing['/'] = $homeParams;
223
            }
224
        }
225
    }
226
227
    /**
228
     * @param string $namespace
229
     * @return bool
230
     */
231 7
    public static function exists($namespace)
232
    {
233 7
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
234
    }
235
236
    /**
237
     * @param string $slug
238
     * @param boolean $absolute
239
     * @param array $params
240
     *
241
     * @return string|null
242
     * @throws RouterException
243
     */
244 2
    public function getRoute($slug = '', $absolute = false, array $params = [])
245
    {
246 2
        $baseUrl = $absolute ? Request::getInstance()->getRootUrl() : '';
247 2
        if ('' === $slug) {
248 1
            return $baseUrl . '/';
249
        }
250 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...
251 1
            throw new RouterException(t('No existe la ruta especificada'));
252
        }
253 2
        $url = $baseUrl . $this->slugs[$slug];
254 2
        if (!empty($params)) {
255 1
            foreach ($params as $key => $value) {
256 1
                $url = str_replace('{' . $key . '}', $value, $url);
257
            }
258 2
        } elseif (!empty($this->routing[$this->slugs[$slug]]['default'])) {
259 2
            $url = $baseUrl . $this->routing[$this->slugs[$slug]]['default'];
260
        }
261
262 2
        return preg_replace('/(GET|POST|PUT|DELETE|ALL|HEAD|PATCH)\#\|\#/', '', $url);
263
    }
264
265
    /**
266
     * @param string $class
267
     * @param string $method
268
     */
269 1
    private function checkPreActions($class, $method)
270
    {
271 1
        $preAction = 'pre' . ucfirst($method);
272 1
        if (method_exists($class, $preAction)) {
273
            Inspector::stats('[Router] Pre action invoked', Inspector::SCOPE_DEBUG);
274
            try {
275
                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 args. ( Ignorable by Annotation )

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

275
                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...
276
                    Logger::log(t('Pre action failed'), LOG_ERR, [error_get_last()]);
277
                    error_clear_last();
278
                }
279
            } catch (Exception $e) {
280
                Logger::log($e->getMessage(), LOG_ERR, [$class, $method]);
281
            }
282
        }
283
    }
284
285
    /**
286
     * @param string $route
287
     * @param array $action
288
     * @param string $class
289
     * @param array $params
290
     * @return mixed
291
     * @throws GeneratorException
292
     * @throws ConfigException
293
     */
294 1
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
295
    {
296 1
        Inspector::stats('[Router] Executing route ' . $route, Inspector::SCOPE_DEBUG);
297 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

297
        $action['params'] = array_merge($action['params'], /** @scrutinizer ignore-type */ $params, Request::getInstance()->getQueryParams());
Loading history...
298 1
        Security::getInstance()->setSessionKey(Cache::CACHE_SESSION_VAR, $action);
299 1
        $cache = Cache::needCache();
300 1
        $execute = TRUE;
301 1
        $return = null;
302 1
        if (FALSE !== $cache && $action['http'] === 'GET' && Config::getParam('debug') === FALSE) {
303
            list($path, $cacheDataName) = $this->cache->getRequestCacheHash();
304
            $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

304
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, /** @scrutinizer ignore-type */ $cache);
Loading history...
305
            if (NULL !== $cachedData) {
306
                $headers = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName . '.headers', $cache, null, Cache::JSON);
307
                Template::getInstance()->renderCache($cachedData, $headers);
308
                $execute = FALSE;
309
            }
310
        }
311 1
        if ($execute) {
312 1
            Inspector::stats('[Router] Start executing action ' . $route, Inspector::SCOPE_DEBUG);
313 1
            $this->checkPreActions($class, $action['method']);
314 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

314
            $return = call_user_func_array([$class, $action['method']], /** @scrutinizer ignore-type */ $params);
Loading history...
315 1
            if (false === $return) {
316
                Logger::log(t('An error occurred trying to execute the action'), LOG_ERR, [error_get_last()]);
317
            }
318
        }
319 1
        return $return;
320
    }
321
322
    /**
323
     * @param Exception|null $exception
324
     * @param bool $isJson
325
     * @return string
326
     * @throws GeneratorException
327
     */
328
    public function httpNotFound(\Exception $exception = null, $isJson = false)
329
    {
330
        return ResponseHelper::httpNotFound($exception, $isJson);
331
    }
332
}
333