Test Failed
Push — master ( 728bda...d24de4 )
by Fran
03:38
created

Router::checkExternalModules()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 18.1823

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 7
nop 0
dl 0
loc 23
ccs 7
cts 18
cp 0.3889
crap 18.1823
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace PSFS\base;
4
5
use PSFS\base\config\Config;
6
use PSFS\base\exception\AccessDeniedException;
7
use PSFS\base\exception\ConfigException;
8
use PSFS\base\exception\RouterException;
9
use PSFS\base\types\helpers\AdminHelper;
10
use PSFS\base\types\helpers\I18nHelper;
11
use PSFS\base\types\helpers\RequestHelper;
12
use PSFS\base\types\helpers\RouterHelper;
13
use PSFS\base\types\helpers\SecurityHelper;
14
use PSFS\base\types\SingletonTrait;
15
use PSFS\controller\base\Admin;
16
use PSFS\services\AdminServices;
17
use Symfony\Component\Finder\Finder;
18
19
20
/**
21
 * Class Router
22
 * @package PSFS
23
 */
24
class Router
25
{
26
27
    use SingletonTrait;
28
29
    protected $routing;
30
    protected $slugs;
31
    private $domains;
32
    /**
33
     * @var Finder $finder
34
     */
35
    private $finder;
36
    /**
37
     * @var \PSFS\base\Cache $cache
38
     */
39
    private $cache;
40
    /**
41
     * @var bool headersSent
42
     */
43
    protected $headersSent = false;
44
45
    /**
46
     * Constructor Router
47
     * @throws ConfigException
48
     */
49 6
    public function __construct()
50
    {
51 6
        $this->finder = new Finder();
52 6
        $this->cache = Cache::getInstance();
53 6
        $this->init();
54 6
    }
55
56
    /**
57
     * Inicializador Router
58
     * @throws ConfigException
59
     */
60 1
    public function init()
61
    {
62 1
        if (!file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json") || Config::getInstance()->getDebugMode()) {
63 1
            $this->hydrateRouting();
64 1
            $this->simpatize();
65 1
        } else {
66 1
            list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json", Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 146 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
67 1
            $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . "domains.json", Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
68
        }
69 1
    }
70
71
    /**
72
     * Método que deriva un error HTTP de página no encontrada
73
     *
74
     * @param \Exception $e
75
     *
76
     * @return string HTML
77
     */
78
    public function httpNotFound(\Exception $e = NULL)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $e. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
79
    {
80
        Logger::log('Throw not found exception');
81
        if (NULL === $e) {
82
            Logger::log('Not found page throwed without previous exception', LOG_WARNING);
83
            $e = new \Exception(_('Page not found'), 404);
84
        }
85
        $template = Template::getInstance()->setStatus($e->getCode());
86
        if (preg_match('/json/i', Request::getInstance()->getServer('CONTENT_TYPE'))) {
87
            return $template->output(json_encode(array(
88
                "success" => FALSE,
89
                "error" => $e->getMessage(),
90
            )), 'application/json');
91
        } else {
92
            return $template->render('error.html.twig', array(
93
                'exception' => $e,
94
                'trace' => $e->getTraceAsString(),
95
                'error_page' => TRUE,
96
            ));
97
        }
98
    }
99
100
    /**
101
     * Método que devuelve las rutas
102
     * @return string|null
103
     */
104
    public function getSlugs()
105
    {
106
        return $this->slugs;
107
    }
108
109
    /**
110
     * Method that extract all routes in the platform
111
     * @return array
112
     */
113
    public function getAllRoutes() {
114
        $routes = [];
115
        foreach($this->routing as $path => $route) {
116
            if(array_key_exists('slug', $route)) {
117
                $routes[$route['slug']] = $path;
118
            }
119
        }
120
        return $routes;
121
    }
122
123
    /**
124
     * Método que calcula el objeto a enrutar
125
     *
126
     * @param string|null $route
127
     *
128
     * @throws \Exception
129
     * @return string HTML
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
130
     */
131
    public function execute($route)
132
    {
133
        Logger::log('Executing the request');
134
        try {
135
            //Check CORS for requests
136
            RequestHelper::checkCORS();
137
            // Checks restricted access
138
            SecurityHelper::checkRestrictedAccess($route);
139
            //Search action and execute
140
            $this->searchAction($route);
141
        } catch (AccessDeniedException $e) {
142
            Logger::log(_('Solicitamos credenciales de acceso a zona restringida'));
143
            return Admin::staticAdminLogon($route);
144
        } catch (RouterException $r) {
145
            Logger::log($r->getMessage(), LOG_WARNING);
146
        } catch (\Exception $e) {
147
            Logger::log($e->getMessage(), LOG_ERR);
148
            throw $e;
149
        }
150
151
        return $this->httpNotFound();
152
    }
153
154
    /**
155
     * Método que busca el componente que ejecuta la ruta
156
     *
157
     * @param string $route
158
     *
159
     * @throws \PSFS\base\exception\RouterException
160
     */
161
    protected function searchAction($route)
162
    {
163
        Logger::log('Searching action to execute: ' . $route, LOG_INFO);
164
        //Revisamos si tenemos la ruta registrada
165
        $parts = parse_url($route);
166
        $path = (array_key_exists('path', $parts)) ? $parts['path'] : $route;
167
        $httpRequest = Request::getInstance()->getMethod();
168
        foreach ($this->routing as $pattern => $action) {
169
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
170
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
171
            if ($matched && ($httpMethod === "ALL" || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 140 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
172
                $get = RouterHelper::extractComponents($route, $routePattern);
173
                /** @var $class \PSFS\base\types\Controller */
174
                $class = RouterHelper::getClassToCall($action);
175
                try {
176
                    $this->executeCachedRoute($route, $action, $class, $get);
177
                } catch (\Exception $e) {
178
                    Logger::log($e->getMessage(), LOG_ERR);
179
                    throw new RouterException($e->getMessage(), 404, $e);
180
                }
181
            }
182
        }
183
        throw new RouterException(_("Ruta no encontrada"));
184
    }
185
186
    /**
187
     * Método que manda las cabeceras de autenticación
188
     * @return string HTML
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
189
     */
190
    protected function sentAuthHeader()
191
    {
192
        return AdminServices::getInstance()->setAdminHeaders();
193
    }
194
195
    private function checkExternalModules() {
196
        $externalModules = Config::getParam('modules.extend');
197
        if(null !== $externalModules) {
198 1
            $externalModules = explode(',', $externalModules);
199
            foreach($externalModules as &$module) {
200 1
                $module = preg_replace('/(\\\|\/)/', DIRECTORY_SEPARATOR, $module);
201 1
                $externalModulePath = VENDOR_DIR . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'src';
202 1
                if(file_exists($externalModulePath)) {
203 1
                    $externalModule = $this->finder->directories()->in($externalModulePath)->depth(0);
204
                    if(!empty($externalModule)) {
205
                        foreach($externalModule as $modulePath) {
206
                            $extModule = $modulePath->getBasename();
207
                            $moduleAutoloader = realpath($externalModulePath . DIRECTORY_SEPARATOR . $extModule . DIRECTORY_SEPARATOR . 'autoload.php');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 152 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
208
                            if(file_exists($moduleAutoloader)) {
209
                                @include $moduleAutoloader;
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
210
                                $this->routing = $this->inspectDir($externalModulePath . DIRECTORY_SEPARATOR . $extModule, $extModule, $this->routing);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 151 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
211
                            }
212
                        }
213
                    }
214
                }
215 1
            }
216 1
        }
217
    }
218
219
    /**
220
     * Method that gather all the routes in the project
221
     */
222 1
    private function generateRouting()
223
    {
224 1
        $base = SOURCE_DIR;
225 1
        $modulesPath = realpath(CORE_DIR);
226 1
        $this->routing = $this->inspectDir($base, "PSFS", array());
227 1
        if (file_exists($modulesPath)) {
228 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
229 1
            foreach($modules as $modulePath) {
230 1
                $module = $modulePath->getBasename();
231
                $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
232
            }
233 1
        }
234 1
        $this->checkExternalModules();
235
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . "domains.json", $this->domains, Cache::JSON, TRUE);
236
    }
237 1
238 1
    /**
239
     * Método que regenera el fichero de rutas
240
     * @throws ConfigException
241
     */
242
    public function hydrateRouting()
243
    {
244
        $this->generateRouting();
245
        $home = Config::getInstance()->get('home_action');
246
        if (NULL !== $home || $home !== '') {
247
            $home_params = NULL;
248
            foreach ($this->routing as $pattern => $params) {
249
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
250 1
                if (preg_match("/" . preg_quote($route, "/") . "$/i", "/" . $home)) {
251
                    $home_params = $params;
252 1
                }
253 1
            }
254 1
            if (NULL !== $home_params) {
255 1
                $this->routing['/'] = $home_params;
256 1
            }
257 1
        }
258
    }
259 1
260
    /**
261
     * Método que inspecciona los directorios en busca de clases que registren rutas
262
     *
263
     * @param string $origen
264
     * @param string $namespace
265
     * @param array $routing
266
     *
267 1
     * @return array
268 1
     * @throws ConfigException
269
     */
270
    private function inspectDir($origen, $namespace = 'PSFS', $routing = [])
271
    {
272
        $files = $this->finder->files()->in($origen)->path('/(controller|api)/i')->depth(1)->name("*.php");
273
        foreach ($files as $file) {
274
            $filename = str_replace("/", '\\', str_replace($origen, '', $file->getPathname()));
275
            $routing = $this->addRouting($namespace . str_replace('.php', '', $filename), $routing, $namespace);
276
        }
277
        $this->finder = new Finder();
278
279
        return $routing;
280
    }
281 1
282
    /**
283 1
     * Checks that a namespace exists
284 1
     * @param string $namespace
285 1
     * @return bool
286 1
     */
287 1
    public static function exists($namespace) {
288 1
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
289 1
    }
290 1
291
    /**
292
     * Método que añade nuevas rutas al array de referencia
293 1
     *
294 1
     * @param string $namespace
295 1
     * @param array $routing
296 1
     * @param string $module
297 1
     *
298 1
     * @return array
299 1
     * @throws ConfigException
300 1
     */
301 1
    private function addRouting($namespace, &$routing, $module = 'PSFS')
302
    {
303 1
        if (self::exists($namespace)) {
304
            $reflection = new \ReflectionClass($namespace);
305
            if (FALSE === $reflection->isAbstract() && FALSE === $reflection->isInterface()) {
306
                $this->extractDomain($reflection);
307
                $classComments = $reflection->getDocComment();
308
                preg_match('/@api\ (.*)\n/im', $classComments, $apiPath);
309
                $api = '';
310
                if (count($apiPath)) {
311
                    $api = array_key_exists(1, $apiPath) ? $apiPath[1] : $api;
312
                }
313
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
314 1
                    list($route, $info) = RouterHelper::extractRouteInfo($method, $api, $module);
315
                    if(null !== $route && null !== $info) {
316
                        $info['class'] = $namespace;
317 1
                        $routing[$route] = $info;
318 1
                    }
319 1
                }
320 1
            }
321 1
        }
322 1
323 1
        return $routing;
324 1
    }
325 1
326
    /**
327 1
     * Método que extrae de la ReflectionClass los datos necesarios para componer los dominios en los templates
328
     *
329
     * @param \ReflectionClass $class
330
     *
331
     * @return Router
332
     * @throws ConfigException
333
     */
334 1
    protected function extractDomain(\ReflectionClass $class)
335
    {
336 1
        //Calculamos los dominios para las plantillas
337 1
        if ($class->hasConstant("DOMAIN") && !$class->isAbstract()) {
338 1
            if(!$this->domains) {
339 1
                $this->domains = [];
340 1
            }
341
            $domain = "@" . $class->getConstant("DOMAIN") . "/";
342 1
            if(!array_key_exists($domain, $this->domains)) {
343
                $this->domains[$domain] = RouterHelper::extractDomainInfo($class, $domain);
344
            }
345
        }
346
347
        return $this;
348
    }
349
350
    /**
351
     * Método que genera las urls amigables para usar dentro del framework
352
     * @return Router
353
     */
354
    public function simpatize()
355 1
    {
356
        $translationFileName = "translations" . DIRECTORY_SEPARATOR . "routes_translations.php";
357 1
        $absoluteTranslationFileName = CACHE_DIR . DIRECTORY_SEPARATOR . $translationFileName;
358
        $this->generateSlugs($absoluteTranslationFileName);
359
        Config::createDir(CONFIG_DIR);
360 1
        Cache::getInstance()->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json", array($this->routing, $this->slugs), Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 144 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
361
362
        return $this;
363 1
    }
364 1
365
    /**
366 1
     * Método que devuelve una ruta del framework
367 1
     *
368 1
     * @param string $slug
369
     * @param boolean $absolute
370 1
     * @param array $params
371
     *
372
     * @return string|null
373
     * @throws RouterException
374
     */
375
    public function getRoute($slug = '', $absolute = FALSE, $params = [])
376
    {
377 1
        if (strlen($slug) === 0) {
378
            return ($absolute) ? Request::getInstance()->getRootUrl() . '/' : '/';
379 1
        }
380
        if (NULL === $slug || !array_key_exists($slug, $this->slugs)) {
381
            throw new RouterException(_("No existe la ruta especificada"));
382
        }
383
        $url = ($absolute) ? Request::getInstance()->getRootUrl() . $this->slugs[$slug] : $this->slugs[$slug];
384
        if (!empty($params)) foreach ($params as $key => $value) {
385
            $url = str_replace("{" . $key . "}", $value, $url);
386
        } elseif (!empty($this->routing[$this->slugs[$slug]]["default"])) {
387
            $url = ($absolute) ? Request::getInstance()->getRootUrl() . $this->routing[$this->slugs[$slug]]["default"] : $this->routing[$this->slugs[$slug]]["default"];
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 168 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
388
        }
389
390
        return preg_replace('/(GET|POST|PUT|DELETE|ALL)\#\|\#/', '', $url);
391
    }
392
393
    /**
394
     * Método que devuelve las rutas de administración
395
     * @return array
396
     */
397
    public function getAdminRoutes()
398
    {
399
        return AdminHelper::getAdminRoutes($this->routing);
400
    }
401
402
    /**
403
     * Método que devuelve le controlador del admin
404
     * @return Admin
405
     */
406
    public function getAdmin()
407
    {
408
        return Admin::getInstance();
409
    }
410
411
    /**
412
     * Método que extrae los dominios
413
     * @return array
414
     */
415
    public function getDomains()
416
    {
417
        return $this->domains ?: [];
418
    }
419
420
    /**
421
     * Método que ejecuta una acción del framework y revisa si lo tenemos cacheado ya o no
422
     *
423
     * @param string $route
424
     * @param array|null $action
425
     * @param types\Controller $class
426
     * @param array $params
0 ignored issues
show
Documentation introduced by
Should the type for parameter $params not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
427
     */
428
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
429
    {
430
        Logger::log('Executing route ' . $route, LOG_INFO);
431
        Security::getInstance()->setSessionKey("__CACHE__", $action);
432
        $cache = Cache::needCache();
433
        $execute = TRUE;
434
        if (FALSE !== $cache && Config::getInstance()->getDebugMode() === FALSE) {
435
            $cacheDataName = $this->cache->getRequestCacheHash();
436
            $tmpDir = substr($cacheDataName, 0, 2) . DIRECTORY_SEPARATOR . substr($cacheDataName, 2, 2) . DIRECTORY_SEPARATOR;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 126 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
437
            $cachedData = $this->cache->readFromCache("json" . DIRECTORY_SEPARATOR . $tmpDir . $cacheDataName,
438
                $cache, function () {});
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 432 can also be of type boolean; however, PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
439 1
            if (NULL !== $cachedData) {
440
                $headers = $this->cache->readFromCache("json" . DIRECTORY_SEPARATOR . $tmpDir . $cacheDataName . ".headers",
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
441 1
                    $cache, function () {}, Cache::JSON);
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 432 can also be of type boolean; however, PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
442 1
                Template::getInstance()->renderCache($cachedData, $headers);
443 1
                $execute = FALSE;
444 1
            }
445 1
        }
446 1
        if ($execute) {
447 1
            Logger::log(_('Start executing action'), LOG_DEBUG);
448 1
            if(false === call_user_func_array(array($class, $action['method']), $params)) {
449 1
                Logger::log(_('An error ocurred trying to execute the action'), LOG_ERR, [error_get_last()]);
450 1
            }
451 1
        }
452 1
    }
453 1
454 1
    /**
455 1
     * Parse slugs to create translations
456 1
     *
457
     * @param string $absoluteTranslationFileName
458
     */
459
    private function generateSlugs($absoluteTranslationFileName)
460
    {
461
        $translations = I18nHelper::generateTranslationsFile($absoluteTranslationFileName);
462
        foreach ($this->routing as $key => &$info) {
463
            $keyParts = $key;
464
            if (FALSE === strstr("#|#", $key)) {
465
                $keyParts = explode("#|#", $key);
466
                $keyParts = array_key_exists(1, $keyParts) ? $keyParts[1] : '';
467
            }
468
            $slug = RouterHelper::slugify($keyParts);
469
            if (NULL !== $slug && !array_key_exists($slug, $translations)) {
470
                $translations[$slug] = $key;
471
                file_put_contents($absoluteTranslationFileName, "\$translations[\"{$slug}\"] = _(\"{$slug}\");\n", FILE_APPEND);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
472
            }
473
            $this->slugs[$slug] = $key;
474
            $info["slug"] = $slug;
475
        }
476
    }
477
478
}
479