Test Failed
Push — master ( ad33a7...e9d973 )
by Fran
08:54
created

Router::extractDomain()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 5
nop 1
dl 0
loc 15
ccs 6
cts 6
cp 1
crap 5
rs 8.8571
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
        $template = Template::getInstance()->setStatus($e->getCode());
82
        if (preg_match('/json/i', Request::getInstance()->getServer('CONTENT_TYPE'))) {
83
            return $template->output(json_encode(array(
84
                "success" => FALSE,
85
                "error" => $e->getMessage(),
86
            )), 'application/json');
87
        } else {
88
            if (NULL === $e) {
89
                Logger::log('Not found page throwed without previous exception', LOG_WARNING);
90
                $e = new \Exception(_('Page not found'), 404);
91
            }
92
93
            return $template->render('error.html.twig', array(
94
                'exception' => $e,
95
                'trace' => $e->getTraceAsString(),
96
                'error_page' => TRUE,
97
            ));
98
        }
99
    }
100
101
    /**
102
     * Método que devuelve las rutas
103
     * @return string|null
104
     */
105
    public function getSlugs()
106
    {
107
        return $this->slugs;
108
    }
109
110
    /**
111
     * Method that extract all routes in the platform
112
     * @return array
113
     */
114
    public function getAllRoutes() {
115
        $routes = [];
116
        foreach($this->routing as $path => $route) {
117
            if(array_key_exists('slug', $route)) {
118
                $routes[$route['slug']] = $path;
119
            }
120
        }
121
        return $routes;
122
    }
123
124
    /**
125
     * Método que calcula el objeto a enrutar
126
     *
127
     * @param string|null $route
128
     *
129
     * @throws \Exception
130
     * @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...
131
     */
132
    public function execute($route)
133
    {
134
        Logger::log('Executing the request');
135
        try {
136
            //Check CORS for requests
137
            RequestHelper::checkCORS();
138
            // Checks restricted access
139
            SecurityHelper::checkRestrictedAccess($route);
140
            //Search action and execute
141
            $this->searchAction($route);
142
        } catch (AccessDeniedException $e) {
143
            Logger::log(_('Solicitamos credenciales de acceso a zona restringida'));
144
            return Admin::staticAdminLogon($route);
145
        } catch (RouterException $r) {
146
            if(null === RouterHelper::checkDefaultRoute($route)) {
147
                Logger::log($r->getMessage(), LOG_WARNING);
148
                throw $r;
149
            }
150
        } catch (\Exception $e) {
151
            Logger::log($e->getMessage(), LOG_ERR);
152
            throw $e;
153
        }
154
155
        return $this->httpNotFound();
156
    }
157
158
    /**
159
     * Método que busca el componente que ejecuta la ruta
160
     *
161
     * @param string $route
162
     *
163
     * @throws \PSFS\base\exception\RouterException
164
     */
165
    protected function searchAction($route)
166
    {
167
        Logger::log('Searching action to execute: ' . $route, LOG_INFO);
168
        //Revisamos si tenemos la ruta registrada
169
        $parts = parse_url($route);
170
        $path = (array_key_exists('path', $parts)) ? $parts['path'] : $route;
171
        $httpRequest = Request::getInstance()->getMethod();
172
        foreach ($this->routing as $pattern => $action) {
173
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
174
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
175
            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...
176
                $get = RouterHelper::extractComponents($route, $routePattern);
177
                /** @var $class \PSFS\base\types\Controller */
178
                $class = RouterHelper::getClassToCall($action);
179
                try {
180
                    $this->executeCachedRoute($route, $action, $class, $get);
181
                } catch (\Exception $e) {
182
                    Logger::log($e->getMessage(), LOG_ERR);
183
                    throw new RouterException($e->getMessage(), 404, $e);
184
                }
185
            }
186
        }
187
        throw new RouterException(_("Ruta no encontrada"));
188
    }
189
190
    /**
191
     * Método que manda las cabeceras de autenticación
192
     * @return string HTML
193
     */
194
    protected function sentAuthHeader()
195
    {
196
        return AdminServices::getInstance()->setAdminHeaders();
197
    }
198
199
    /**
200
     * Method that gather all the routes in the project
201
     */
202 1
    private function generateRouting()
203
    {
204 1
        $base = SOURCE_DIR;
205 1
        $modules = realpath(CORE_DIR);
206 1
        $this->routing = $this->inspectDir($base, "PSFS", array());
207 1
        if (file_exists($modules)) {
208
            $module = "";
209
            if(file_exists($modules . DIRECTORY_SEPARATOR . 'module.json')) {
210
                $mod_cfg = json_decode(file_get_contents($modules . DIRECTORY_SEPARATOR . 'module.json'), true);
211
                $module = $mod_cfg['module'];
212
            }
213
            $this->routing = $this->inspectDir($modules, $module, $this->routing);
214
        }
215 1
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . "domains.json", $this->domains, Cache::JSON, TRUE);
216 1
    }
217
218
    /**
219
     * Método que regenera el fichero de rutas
220
     * @throws ConfigException
221
     */
222 1
    public function hydrateRouting()
223
    {
224 1
        $this->generateRouting();
225 1
        $home = Config::getInstance()->get('home_action');
226 1
        if (NULL !== $home || $home !== '') {
227 1
            $home_params = NULL;
228 1
            foreach ($this->routing as $pattern => $params) {
229 1
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
230 1
                if (preg_match("/" . preg_quote($route, "/") . "$/i", "/" . $home)) {
231
                    $home_params = $params;
232
                }
233 1
            }
234 1
            if (NULL !== $home_params) {
235
                $this->routing['/'] = $home_params;
236
            }
237 1
        }
238 1
    }
239
240
    /**
241
     * Método que inspecciona los directorios en busca de clases que registren rutas
242
     *
243
     * @param string $origen
244
     * @param string $namespace
245
     * @param array $routing
246
     *
247
     * @return array
248
     * @throws ConfigException
249
     */
250 1
    private function inspectDir($origen, $namespace = 'PSFS', $routing)
251
    {
252 1
        $files = $this->finder->files()->in($origen)->path('/(controller|api)/i')->name("*.php");
253 1
        foreach ($files as $file) {
254 1
            $filename = str_replace("/", '\\', str_replace($origen, '', $file->getPathname()));
255 1
            $routing = $this->addRouting($namespace . str_replace('.php', '', $filename), $routing);
256 1
        }
257 1
        $this->finder = new Finder();
258
259 1
        return $routing;
260
    }
261
262
    /**
263
     * Checks that a namespace exists
264
     * @param string $namespace
265
     * @return bool
266
     */
267 1
    public static function exists($namespace) {
268 1
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
269
    }
270
271
    /**
272
     * Método que añade nuevas rutas al array de referencia
273
     *
274
     * @param string $namespace
275
     * @param array $routing
276
     *
277
     * @return array
278
     * @throws ConfigException
279
     */
280 1
    private function addRouting($namespace, &$routing)
281
    {
282 1
        if (self::exists($namespace)) {
283 1
            $reflection = new \ReflectionClass($namespace);
284 1
            if (FALSE === $reflection->isAbstract() && FALSE === $reflection->isInterface()) {
285 1
                $this->extractDomain($reflection);
286 1
                $classComments = $reflection->getDocComment();
287 1
                preg_match('/@api\ (.*)\n/im', $classComments, $apiPath);
288 1
                $api = '';
289 1
                if (count($apiPath)) {
290
                    $api = array_key_exists(1, $apiPath) ? $apiPath[1] : $api;
291
                }
292 1
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
293 1
                    list($route, $info) = RouterHelper::extractRouteInfo($method, $api);
294 1
                    if(null !== $route && null !== $info) {
295 1
                        $info['class'] = $namespace;
296 1
                        $routing[$route] = $info;
297 1
                    }
298 1
                }
299 1
            }
300 1
        }
301
302 1
        return $routing;
303
    }
304
305
    /**
306
     * Método que extrae de la ReflectionClass los datos necesarios para componer los dominios en los templates
307
     *
308
     * @param \ReflectionClass $class
309
     *
310
     * @return Router
311
     * @throws ConfigException
312
     */
313 1
    protected function extractDomain(\ReflectionClass $class)
314
    {
315
        //Calculamos los dominios para las plantillas
316 1
        if ($class->hasConstant("DOMAIN") && !$class->isAbstract()) {
317 1
            if(!$this->domains) {
318 1
                $this->domains = [];
319 1
            }
320
            $domain = "@" . $class->getConstant("DOMAIN") . "/";
321 1
            if(!array_key_exists($domain, $this->domains)) {
322
                $this->domains[$domain] = RouterHelper::extractDomainInfo($class, $domain);
323
            }
324
        }
325
326
        return $this;
327
    }
328 1
329
    /**
330 1
     * Método que genera las urls amigables para usar dentro del framework
331 1
     * @return Router
332 1
     */
333 1
    public function simpatize()
334 1
    {
335
        $translationFileName = "translations" . DIRECTORY_SEPARATOR . "routes_translations.php";
336 1
        $absoluteTranslationFileName = CACHE_DIR . DIRECTORY_SEPARATOR . $translationFileName;
337
        $this->generateSlugs($absoluteTranslationFileName);
338
        Config::createDir(CONFIG_DIR);
339
        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...
340
341
        return $this;
342
    }
343
344
    /**
345
     * Método que devuelve una ruta del framework
346
     *
347
     * @param string $slug
348
     * @param boolean $absolute
349
     * @param array $params
350
     *
351
     * @return string|null
352
     * @throws RouterException
353
     */
354
    public function getRoute($slug = '', $absolute = FALSE, $params = [])
355
    {
356
        if (strlen($slug) === 0) {
357
            return ($absolute) ? Request::getInstance()->getRootUrl() . '/' : '/';
358
        }
359
        if (NULL === $slug || !array_key_exists($slug, $this->slugs)) {
360
            throw new RouterException(_("No existe la ruta especificada"));
361
        }
362
        $url = ($absolute) ? Request::getInstance()->getRootUrl() . $this->slugs[$slug] : $this->slugs[$slug];
363
        if (!empty($params)) foreach ($params as $key => $value) {
364
            $url = str_replace("{" . $key . "}", $value, $url);
365
        } elseif (!empty($this->routing[$this->slugs[$slug]]["default"])) {
366
            $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...
367
        }
368
369
        return preg_replace('/(GET|POST|PUT|DELETE|ALL)\#\|\#/', '', $url);
370
    }
371 1
372
    /**
373 1
     * Método que devuelve las rutas de administración
374
     * @return array
375
     */
376
    public function getAdminRoutes()
377
    {
378
        return AdminHelper::getAdminRoutes($this->routing);
379
    }
380
381
    /**
382
     * Método que devuelve le controlador del admin
383
     * @return Admin
384
     */
385
    public function getAdmin()
386
    {
387
        return Admin::getInstance();
388
    }
389
390
    /**
391
     * Método que extrae los dominios
392
     * @return array
393
     */
394
    public function getDomains()
395
    {
396
        return $this->domains ?: [];
397
    }
398
399
    /**
400
     * Método que ejecuta una acción del framework y revisa si lo tenemos cacheado ya o no
401
     *
402
     * @param string $route
403
     * @param array|null $action
404
     * @param types\Controller $class
405
     * @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...
406
     */
407
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
408
    {
409
        Logger::log('Executing route ' . $route, LOG_INFO);
410
        Security::getInstance()->setSessionKey("__CACHE__", $action);
411
        $cache = Cache::needCache();
412
        $execute = TRUE;
413
        if (FALSE !== $cache && Config::getInstance()->getDebugMode() === FALSE) {
414
            $cacheDataName = $this->cache->getRequestCacheHash();
415
            $cachedData = $this->cache->readFromCache("templates" . DIRECTORY_SEPARATOR . $cacheDataName,
416
                $cache, function () {});
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 411 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...
417
            if (NULL !== $cachedData) {
418
                $headers = $this->cache->readFromCache("templates" . DIRECTORY_SEPARATOR . $cacheDataName . ".headers",
419
                    $cache, function () {}, Cache::JSON);
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 411 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...
420
                Template::getInstance()->renderCache($cachedData, $headers);
421
                $execute = FALSE;
422
            }
423
        }
424
        if ($execute) {
425
            call_user_func_array(array($class, $action['method']), $params);
426
        }
427
    }
428
429 1
    /**
430
     * Parse slugs to create translations
431 1
     *
432 1
     * @param string $absoluteTranslationFileName
433 1
     */
434 1
    private function generateSlugs($absoluteTranslationFileName)
435 1
    {
436 1
        $translations = I18nHelper::generateTranslationsFile($absoluteTranslationFileName);
437 1
        foreach ($this->routing as $key => &$info) {
438 1
            $keyParts = $key;
439 1
            if (FALSE === strstr("#|#", $key)) {
440 1
                $keyParts = explode("#|#", $key);
441 1
                $keyParts = array_key_exists(1, $keyParts) ? $keyParts[1] : '';
442 1
            }
443 1
            $slug = RouterHelper::slugify($keyParts);
444 1
            if (NULL !== $slug && !array_key_exists($slug, $translations)) {
445 1
                $translations[$slug] = $key;
446 1
                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...
447
            }
448
            $this->slugs[$slug] = $key;
449
            $info["slug"] = $slug;
450
        }
451
    }
452
453
}
454