Passed
Push — master ( 308456...27f142 )
by Fran
03:14
created

Security   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 93.62%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 421
ccs 132
cts 141
cp 0.9362
rs 6.3005
c 1
b 1
f 0
wmc 58
lcom 1
cbo 7

26 Methods

Rating   Name   Duplication   Size   Complexity  
B init() 0 15 6
A initSession() 0 9 4
A getProfiles() 0 8 1
A getAdminProfiles() 0 4 1
A getCleanProfiles() 0 8 1
A getAdminCleanProfiles() 0 4 1
A save() 0 15 3
A saveUser() 0 8 2
A updateUser() 0 4 1
A getAdmins() 0 4 1
C checkAdmin() 0 41 13
A getAdminFromCookie() 0 10 2
A getHash() 0 4 1
A getUser() 0 4 1
A getAdmin() 0 4 1
A canAccessRestrictedAdmin() 0 4 2
A notAuthorized() 0 6 1
A isSuperAdmin() 0 11 2
A getSessionKey() 0 9 2
A setSessionKey() 0 6 1
A getFlashes() 0 6 2
A clearFlashes() 0 6 1
A setFlash() 0 9 2
A getFlash() 0 6 3
A updateSession() 0 14 2
A closeSession() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like Security often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Security, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace PSFS\base;
3
4
use PSFS\base\types\helpers\RequestHelper;
5
use PSFS\base\types\helpers\ResponseHelper;
6
use PSFS\base\types\traits\SecureTrait;
7
use PSFS\base\types\traits\SingletonTrait;
8
9
/**
10
 * Class Security
11
 * @package PSFS
12
 */
13
class Security
14
{
15
    // sha1('user')
16
    const USER_ID_TOKEN = '12dea96fec20593566ab75692c9949596833adc9';
17
    // sha1('admin')
18
    const MANAGER_ID_TOKEN = 'd033e22ae348aeb5660fc2140aec35850c4da997';
19
    // sha1('superadmin')
20
    const ADMIN_ID_TOKEN = '889a3a791b3875cfae413574b53da4bb8a90d53e';
21
    // sha1('FLASHES')
22
    const FLASH_MESSAGE_TOKEN = '4680c68435db1bfbf17c3fcc4f7b39d2c6122504';
23
24
    use SecureTrait;
25
    use SingletonTrait;
26
    /**
27
     * @var array $user
28
     */
29
    private $user = null;
30
31
    /**
32
     * @var array $admin
33
     */
34
    private $admin = null;
35
36
    /**
37
     * @var bool $authorized
38
     */
39
    private $authorized = FALSE;
40
41
    /**
42
     * @var bool $checked
43
     */
44
    private $checked = false;
45
46
    /**
47
     * @var array $session
48
     */
49
    protected $session;
50
51
    /**
52
     * Constructor por defecto
53
     */
54 2
    public function init()
0 ignored issues
show
Coding Style introduced by
init uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
55
    {
56 2
        $this->initSession();
57 2
        $this->session = (is_null($_SESSION)) ? array() : $_SESSION;
58 2
        if (NULL === $this->getSessionKey('__FLASH_CLEAR__')) {
59 2
            $this->clearFlashes();
60 2
            $this->setSessionKey('__FLASH_CLEAR__', microtime(TRUE));
61
        }
62 2
        $this->user = (array_key_exists(self::USER_ID_TOKEN, $this->session)) ? unserialize($this->session[self::USER_ID_TOKEN]) : NULL;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 136 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...
63 2
        $this->admin = (array_key_exists(self::ADMIN_ID_TOKEN, $this->session)) ? unserialize($this->session[self::ADMIN_ID_TOKEN]) : NULL;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 139 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...
64 2
        if (null === $this->admin) {
65 2
            $this->checkAdmin();
66
        }
67 2
        $this->setLoaded(true);
68 2
    }
69
70
    /**
71
     * Initializator for SESSION
72
     */
73 2
    private function initSession() {
0 ignored issues
show
Coding Style introduced by
initSession uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
74 2
        if (PHP_SESSION_NONE === session_status() && !headers_sent()) {
75
            session_start();
76
        }
77
        // Fix for phpunits
78 2
        if(!isset($_SESSION)) {
79 1
            $_SESSION = [];
80
        }
81 2
    }
82
83
    /**
84
     * Método estático que devuelve los perfiles de la plataforma
85
     * @return array
86
     */
87 2
    public static function getProfiles()
88
    {
89
        return array(
90 2
            self::ADMIN_ID_TOKEN => _('Administrador'),
91 2
            self::MANAGER_ID_TOKEN => _('Gestor'),
92 2
            self::USER_ID_TOKEN => _('Usuario'),
93
        );
94
    }
95
96
    /**
97
     * Method that returns all the available profiles
98
     * @return array
99
     */
100 1
    public function getAdminProfiles()
101
    {
102 1
        return static::getProfiles();
103
    }
104
105
    /**
106
     * Método estático que devuelve los perfiles disponibles
107
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
108
     */
109 2
    public static function getCleanProfiles()
110
    {
111
        return array(
112 2
            '__SUPER_ADMIN__' => self::ADMIN_ID_TOKEN,
113 2
            '__ADMIN__' => self::MANAGER_ID_TOKEN,
114 2
            '__USER__' => self::USER_ID_TOKEN,
115
        );
116
    }
117
118
    /**
119
     * Método estático que devuelve los perfiles disponibles
120
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
121
     */
122 1
    public function getAdminCleanProfiles()
123
    {
124 1
        return static::getCleanProfiles();
125
    }
126
127
    /**
128
     * Método que guarda los administradores
129
     *
130
     * @param array $user
131
     *
132
     * @return bool
133
     */
134 1
    public static function save($user)
135
    {
136 1
        $saved = true;
137
        try {
138 1
            $admins = Cache::getInstance()->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json', Cache::JSONGZ, true) ?: [];
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 137 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...
139 1
            $admins[$user['username']]['hash'] = sha1($user['username'] . $user['password']);
140 1
            $admins[$user['username']]['profile'] = $user['profile'];
141
142 1
            Cache::getInstance()->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json', $admins, Cache::JSONGZ, true);
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...
143
        } catch(\Exception $e) {
144
            Logger::log($e->getMessage(), LOG_ERR);
145
            $saved = false;
146
        }
147 1
        return $saved;
148
    }
149
150
    /**
151
     * Method to save a new admin user
152
     * @param array $user
153
     * @return bool
154
     */
155 1
    public function saveUser($user)
156
    {
157 1
        $saved = false;
158 1
        if (!empty($user)) {
159 1
            $saved = static::save($user);
160
        }
161 1
        return $saved;
162
    }
163
164
    /**
165
     * Servicio que actualiza los datos del usuario
166
     *
167
     * @param $user
168
     */
169 1
    public function updateUser($user)
170
    {
171 1
        $this->user = $user;
172 1
    }
173
174
    /**
175
     * Método que devuelve los administradores de una plataforma
176
     * @return array|null
0 ignored issues
show
Documentation introduced by
Should the return type not be array|string|null? Also, consider making the array more specific, something like array<String>, or String[].

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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
177
     */
178 4
    public function getAdmins()
179
    {
180 4
        return Cache::getInstance()->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json', Cache::JSONGZ, true);
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...
181
    }
182
183
    /**
184
     * Método que devuelve si un usuario tiene privilegios para acceder a la zona de administración
185
     *
186
     * @param null $user
187
     * @param null $pass
188
     * @param boolean $force
189
     *
190
     * @return bool
191
     * @throws \HttpException
192
     */
193 3
    public function checkAdmin($user = NULL, $pass = NULL, $force = false)
194
    {
195 3
        Logger::log('Checking admin session');
196 3
        if ((!$this->authorized && !$this->checked) || $force) {
197 3
            $admins = $this->getAdmins();
198 3
            if (null !== $admins) {
199 1
                $request = Request::getInstance();
200
                //Sacamos las credenciales de la petición
201 1
                $user = $user ?: $request->getServer('PHP_AUTH_USER');
202 1
                $pass = $pass ?: $request->getServer('PHP_AUTH_PW');
203 1
                if (NULL === $user || (array_key_exists($user, $admins) && empty($admins[$user]))) {
204 1
                    list($user, $pass) = $this->getAdminFromCookie();
205
                }
206 1
                if (!empty($user) && !empty($admins[$user])) {
207 1
                    $auth = $admins[$user]['hash'];
208 1
                    $this->authorized = ($auth == sha1($user . $pass));
209 1
                    if ($this->authorized) {
210 1
                        $this->admin = array(
211 1
                            'alias' => $user,
212 1
                            'profile' => $admins[$user]['profile'],
213
                        );
214 1
                        $this->setSessionKey(self::ADMIN_ID_TOKEN, serialize($this->admin));
215 1
                        ResponseHelper::setCookieHeaders([
216
                            [
217 1
                                'name' => $this->getHash(),
218 1
                                'value' => base64_encode("$user:$pass"),
219
                                'http' => true,
220 1
                                'domain' => '',
221
                            ]
222
                        ]);
223
                    }
224
                } else {
225 1
                    $this->admin = null;
226 1
                    $this->setSessionKey(self::ADMIN_ID_TOKEN, null);
227
                }
228 1
                $this->checked = true;
229
            }
230
        }
231
232 3
        return $this->authorized;
233
    }
234
235
    /**
236
     * Método que obtiene el usuario y contraseña de la cookie de sesión de administración
237
     * @return array
238
     */
239 1
    protected function getAdminFromCookie()
240
    {
241 1
        $auth_cookie = Request::getInstance()->getCookie($this->getHash());
242 1
        $user = $pass = array();
243 1
        if (!empty($auth_cookie)) {
244 1
            list($user, $pass) = explode(':', base64_decode($auth_cookie));
245
        }
246
247 1
        return array($user, $pass);
248
    }
249
250
    /**
251
     * Método privado para la generación del hash de la cookie de administración
252
     * @return string
253
     */
254 1
    public function getHash()
255
    {
256 1
        return substr(self::MANAGER_ID_TOKEN, 0, 8);
257
    }
258
259
    /**
260
     * Método que devuelve el usuario logado
261
     * @return array
262
     */
263 2
    public function getUser()
264
    {
265 2
        return $this->user;
266
    }
267
268
    /**
269
     * Método que devuelve el usuario administrador logado
270
     * @return array
271
     */
272 2
    public function getAdmin()
273
    {
274 2
        return $this->admin;
275
    }
276
277
    /**
278
     * Método que calcula si se está logado o para acceder a administración
279
     * @return bool
280
     */
281 3
    public function canAccessRestrictedAdmin()
282
    {
283 3
        return null !== $this->admin && !preg_match('/^\/admin\/login/i', Request::requestUri());
284
    }
285
286
    /**
287
     * Servicio que devuelve una pantalla de error porque se necesita estar authenticado
288
     *
289
     * @param string|null $route
290
     *
291
     * @return string|null
292
     */
293
    public function notAuthorized($route)
294
    {
295
        return Template::getInstance()->render('notauthorized.html.twig', array(
296
            'route' => $route,
297
        ));
298
    }
299
300
    /**
301
     * Servicio que chequea si un usuario es super administrador o no
302
     * @return bool
303
     */
304 1
    public function isSuperAdmin()
305
    {
306 1
        $users = $this->getAdmins();
307 1
        $logged = $this->getAdmin();
308 1
        if ($users[$logged['alias']]) {
309 1
            $security = $users[$logged['alias']]['profile'];
310 1
            return self::ADMIN_ID_TOKEN === $security;
311
        }
312
313
        return FALSE;
314
    }
315
316
    /**
317
     * Servicio que devuelve un dato de sesión
318
     *
319
     * @param string $key
320
     *
321
     * @return mixed
322
     */
323 6
    public function getSessionKey($key)
324
    {
325 6
        $data = NULL;
326 6
        if (array_key_exists($key, $this->session)) {
327 4
            $data = $this->session[$key];
328
        }
329
330 6
        return $data;
331
    }
332
333
    /**
334
     * Servicio que setea una variable de sesión
335
     *
336
     * @param string $key
337
     * @param mixed $data
338
     *
339
     * @return Security
340
     */
341 5
    public function setSessionKey($key, $data = NULL)
342
    {
343 5
        $this->session[$key] = $data;
344
345 5
        return $this;
346
    }
347
348
    /**
349
     * Servicio que devuelve los mensajes flash de sesiones
350
     * @return mixed
351
     */
352 2
    public function getFlashes()
353
    {
354 2
        $flashes = $this->getSessionKey(self::FLASH_MESSAGE_TOKEN);
355
356 2
        return (NULL !== $flashes) ? $flashes : array();
357
    }
358
359
    /**
360
     * Servicio que limpia los mensajes flash
361
     * @return $this
362
     */
363 3
    public function clearFlashes()
364
    {
365 3
        $this->setSessionKey(self::FLASH_MESSAGE_TOKEN, NULL);
366
367 3
        return $this;
368
    }
369
370
    /**
371
     * Servicio que inserta un flash en sesión
372
     *
373
     * @param string $key
374
     * @param mixed $data
375
     */
376 1
    public function setFlash($key, $data = NULL)
377
    {
378 1
        $flashes = $this->getFlashes();
379 1
        if (!is_array($flashes)) {
380
            $flashes = [];
381
        }
382 1
        $flashes[$key] = $data;
383 1
        $this->setSessionKey(self::FLASH_MESSAGE_TOKEN, $flashes);
384 1
    }
385
386
    /**
387
     * Servicio que devuelve un flash de sesión
388
     *
389
     * @param string $key
390
     *
391
     * @return mixed
392
     */
393 2
    public function getFlash($key)
394
    {
395 2
        $flashes = $this->getFlashes();
396
397 2
        return (NULL !== $key && array_key_exists($key, $flashes)) ? $flashes[$key] : NULL;
398
    }
399
400
    /**
401
     * Servicio que actualiza
402
     *
403
     * @param boolean $closeSession
404
     *
405
     * @return Security
406
     */
407 2
    public function updateSession($closeSession = FALSE)
0 ignored issues
show
Coding Style introduced by
updateSession uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
408
    {
409 2
        Logger::log('Update session');
410 2
        $_SESSION = $this->session;
411 2
        $_SESSION[self::USER_ID_TOKEN] = serialize($this->user);
412 2
        $_SESSION[self::ADMIN_ID_TOKEN] = serialize($this->admin);
413 2
        if ($closeSession) {
414 1
            Logger::log('Close session');
415 1
            @session_write_close();
416 1
            @session_start();
417
        }
418 2
        Logger::log('Session updated');
419 2
        return $this;
420
    }
421
422
    /**
423
     * Servicio que limpia la sesión
424
     */
425 1
    public function closeSession()
0 ignored issues
show
Coding Style introduced by
closeSession uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
426
    {
427 1
        unset($_SESSION);
428 1
        @session_destroy();
429 1
        @session_regenerate_id(TRUE);
430 1
        @session_start();
431 1
    }
432
433
}
434