Passed
Push — master ( 1c0893...f7ffcc )
by Fran
03:22
created

Security   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 486
Duplicated Lines 1.23 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 14.29%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 6
loc 486
ccs 27
cts 189
cp 0.1429
rs 5.7894
c 2
b 0
f 0
wmc 65
lcom 2
cbo 5

30 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 13 6
A getProfiles() 0 8 1
A getAdminProfiles() 0 4 1
A getCleanProfiles() 0 8 1
A getAdminCleanProfiles() 0 4 1
A save() 3 11 2
A saveUser() 0 8 2
A updateUser() 0 4 1
A getAdmins() 3 9 2
D checkAdmin() 0 28 10
A getAdminFromCookie() 0 10 2
A getHash() 0 4 1
A getAdmin() 0 4 1
A notAuthorized() 0 6 1
A isSuperAdmin() 0 13 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 13 2
A closeSession() 0 5 1
A extractTokenParts() 0 16 3
A extractTsAndMod() 0 13 3
A decodeToken() 0 11 3
A generateToken() 0 15 2
A checkToken() 0 11 3
A getUser() 0 4 1
A canAccessRestrictedAdmin() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
3
namespace PSFS\base;
4
5
use PSFS\base\types\SingletonTrait;
6
use PSFS\controller\UserController;
7
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 SingletonTrait;
25
    /**
26
     * @var array $user
27
     */
28
    private $user;
29
30
    /**
31
     * @var array $admin
32
     */
33
    private $admin;
34
35
    private $authorized = FALSE;
36
37
    protected $session;
38
39
    /**
40
     * Constructor por defecto
41
     */
42 3
    public function __construct()
43
    {
44 3
        if (PHP_SESSION_NONE === session_status()) {
45 1
            session_start();
46
        }
47 2
        $this->session = (is_null($_SESSION)) ? array() : $_SESSION;
48 2
        if (NULL === $this->getSessionKey('__FLASH_CLEAR__')) {
49 2
            $this->clearFlashes();
50 2
            $this->setSessionKey('__FLASH_CLEAR__', microtime(TRUE));
51 2
        }
52 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...
53 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...
54 2
    }
55
56
    /**
57
     * Método estático que devuelve los perfiles de la plataforma
58
     * @return array
59
     */
60
    public static function getProfiles()
61
    {
62
        return array(
63
            self::ADMIN_ID_TOKEN => _('Administrador'),
64
            self::MANAGER_ID_TOKEN => _('Gestor'),
65
            self::USER_ID_TOKEN => _('Usuario'),
66
        );
67
    }
68
69
    /**
70
     * Method that returns all the available profiles
71
     * @return array
72
     */
73
    public function getAdminProfiles()
74
    {
75
        return static::getProfiles();
76
    }
77
78
    /**
79
     * Método estático que devuelve los perfiles disponibles
80
     * @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...
81
     */
82
    public static function getCleanProfiles()
83
    {
84
        return array(
85
            '__SUPER_ADMIN__' => self::ADMIN_ID_TOKEN,
86
            '__ADMIN__' => self::MANAGER_ID_TOKEN,
87
            '__USER__' => self::USER_ID_TOKEN,
88
        );
89
    }
90
91
    /**
92
     * Método estático que devuelve los perfiles disponibles
93
     * @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...
94
     */
95
    public function getAdminCleanProfiles()
96
    {
97
        return static::getCleanProfiles();
98
    }
99
100
    /**
101
     * Método que guarda los administradores
102
     *
103
     * @param array $user
104
     *
105
     * @return bool
106
     */
107
    public static function save($user)
108
    {
109
        $admins = array();
110 View Code Duplication
        if (file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
111
            $admins = json_decode(file_get_contents(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json'), TRUE);
112
        }
113
        $admins[$user['username']]['hash'] = sha1($user['username'] . $user['password']);
114
        $admins[$user['username']]['profile'] = $user['profile'];
115
116
        return (FALSE !== file_put_contents(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json', json_encode($admins, JSON_PRETTY_PRINT)));
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...
117
    }
118
119
    /**
120
     * Method to save a new admin user
121
     * @param array $user
122
     * @return bool
123
     */
124
    public function saveUser($user)
125
    {
126
        $saved = false;
127
        if (!empty($user)) {
128
            $saved = static::save($user);
129
        }
130
        return $saved;
131
    }
132
133
    /**
134
     * Servicio que actualiza los datos del usuario
135
     *
136
     * @param $user
137
     */
138
    public function updateUser($user)
139
    {
140
        $this->user = $user;
141
    }
142
143
    /**
144
     * Método que devuelve los administradores de una plataforma
145
     * @return array|mixed
146
     */
147
    public function getAdmins()
148
    {
149
        $admins = array();
150 View Code Duplication
        if (file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
151
            $admins = json_decode(file_get_contents(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json'), TRUE);
152
        }
153
154
        return $admins;
155
    }
156
157
    /**
158
     * Método que devuelve si un usuario tiene privilegios para acceder a la zona de administración
159
     *
160
     * @param null $user
161
     * @param null $pass
162
     *
163
     * @return bool
164
     * @throws \HttpException
165
     */
166
    public function checkAdmin($user = NULL, $pass = NULL)
167
    {
168
        Logger::log('Checking admin session');
169
        if (!$this->authorized) {
170
            $request = Request::getInstance();
171
            if (!file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
172
                //Si no hay fichero de usuarios redirigimos directamente al gestor
173
                return UserController::getInstance()->adminers();
174
            }
175
            $admins = $this->getAdmins();
176
            //Sacamos las credenciales de la petición
177
            $user = $user ?: $request->getServer('PHP_AUTH_USER');
178
            $pass = $pass ?: $request->getServer('PHP_AUTH_PW');
179
            if (NULL === $user || (array_key_exists($user, $admins) && empty($admins[$user]))) {
180
                list($user, $pass) = $this->getAdminFromCookie();
181
            }
182
            if (!empty($user) && !empty($admins[$user])) {
183
                $auth = $admins[$user]['hash'];
184
                $this->authorized = ($auth == sha1($user . $pass));
185
                $this->admin = array(
186
                    'alias' => $user,
187
                    'profile' => $admins[$user]['profile'],
188
                );
189
            }
190
        }
191
192
        return $this->authorized;
193
    }
194
195
    /**
196
     * Método que obtiene el usuario y contraseña de la cookie de sesión de administración
197
     * @return array
198
     */
199
    protected function getAdminFromCookie()
200
    {
201
        $auth_cookie = Request::getInstance()->getCookie($this->getHash());
202
        $user = $pass = array();
203
        if (!empty($auth_cookie)) {
204
            list($user, $pass) = explode(':', base64_decode($auth_cookie));
205
        }
206
207
        return array($user, $pass);
208
    }
209
210
    /**
211
     * Método privado para la generación del hash de la cookie de administración
212
     * @return string
213
     */
214
    public function getHash()
215
    {
216
        return substr(md5('admin'), 0, 8);
217
    }
218
219
    /**
220
     * Método que devuelve el usuario logado
221
     * @return array
222
     */
223 1
    public function getUser()
224
    {
225 1
        return $this->user;
226
    }
227
228
    /**
229
     * Método que devuelve el usuario administrador logado
230
     * @return array
231
     */
232
    public function getAdmin()
233
    {
234
        return $this->admin;
235
    }
236
237
    /**
238
     * Método que calcula si se está logado o para acceder a administración
239
     * @return bool
240
     */
241 1
    public function canAccessRestrictedAdmin()
242
    {
243 1
        return $this->admin || preg_match('/^\/admin\/login/i', Request::requestUri());
244
    }
245
246
    /**
247
     * Servicio que devuelve una pantalla de error porque se necesita estar authenticado
248
     *
249
     * @param string|null $route
250
     *
251
     * @return string|null
252
     */
253
    public function notAuthorized($route)
254
    {
255
        return Template::getInstance()->render('notauthorized.html.twig', array(
256
            'route' => $route,
257
        ));
258
    }
259
260
    /**
261
     * Servicio que chequea si un usuario es super administrador o no
262
     * @return bool
263
     */
264
    public function isSuperAdmin()
265
    {
266
        $users = $this->getAdmins();
267
        $logged = $this->getAdminFromCookie();
268
        $profiles = Security::getCleanProfiles();
269
        if ($users[$logged[0]]) {
270
            $security = $users[$logged[0]]['profile'];
271
272
            return $profiles['__SUPER_ADMIN__'] === $security;
273
        }
274
275
        return FALSE;
276
    }
277
278
    /**
279
     * Servicio que devuelve un dato de sesión
280
     *
281
     * @param string $key
282
     *
283
     * @return mixed
284
     */
285 2
    public function getSessionKey($key)
286
    {
287 2
        $data = NULL;
288 2
        if (array_key_exists($key, $this->session)) {
289 1
            $data = $this->session[$key];
290 1
        }
291
292 2
        return $data;
293
    }
294
295
    /**
296
     * Servicio que setea una variable de sesión
297
     *
298
     * @param string $key
299
     * @param mixed $data
300
     *
301
     * @return Security
302
     */
303 2
    public function setSessionKey($key, $data = NULL)
304
    {
305 2
        $this->session[$key] = $data;
306
307 2
        return $this;
308
    }
309
310
    /**
311
     * Servicio que devuelve los mensajes flash de sesiones
312
     * @return mixed
313
     */
314
    public function getFlashes()
315
    {
316
        $flashes = $this->getSessionKey(self::FLASH_MESSAGE_TOKEN);
317
318
        return (NULL !== $flashes) ? $flashes : array();
319
    }
320
321
    /**
322
     * Servicio que limpia los mensajes flash
323
     * @return $this
324
     */
325 1
    public function clearFlashes()
326
    {
327 1
        $this->setSessionKey(self::FLASH_MESSAGE_TOKEN, NULL);
328
329 1
        return $this;
330
    }
331
332
    /**
333
     * Servicio que inserta un flash en sesión
334
     *
335
     * @param string $key
336
     * @param mixed $data
337
     */
338
    public function setFlash($key, $data = NULL)
339
    {
340
        $flashes = $this->getFlashes();
341
        if (!is_array($flashes)) {
342
            $flashes = array();
343
        }
344
        $flashes[$key] = $data;
345
        $this->setSessionKey(self::FLASH_MESSAGE_TOKEN, $flashes);
346
    }
347
348
    /**
349
     * Servicio que devuelve un flash de sesión
350
     *
351
     * @param string $key
352
     *
353
     * @return mixed
354
     */
355
    public function getFlash($key)
356
    {
357
        $flashes = $this->getFlashes();
358
359
        return (NULL !== $key && array_key_exists($key, $flashes)) ? $flashes[$key] : NULL;
360
    }
361
362
    /**
363
     * Servicio que actualiza
364
     *
365
     * @param boolean $closeSession
366
     *
367
     * @return Security
368
     */
369
    public function updateSession($closeSession = FALSE)
370
    {
371
        Logger::log('Update session');
372
        $_SESSION = $this->session;
373
        $_SESSION[self::USER_ID_TOKEN] = serialize($this->user);
374
        $_SESSION[self::ADMIN_ID_TOKEN] = serialize($this->admin);
375
        if ($closeSession) {
376
            Logger::log('Close session');
377
            session_write_close();
378
        }
379
        Logger::log('Session updated');
380
        return $this;
381
    }
382
383
    /**
384
     * Servicio que limpia la sesión
385
     */
386
    public function closeSession()
387
    {
388
        session_destroy();
389
        session_regenerate_id(TRUE);
390
    }
391
392
    /**
393
     * Extract parts from token
394
     * @param string $token
395
     *
396
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<double|integer|array>.

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...
397
     */
398
    private static function extractTokenParts($token)
399
    {
400
        $axis = 0;
401
        $parts = array();
402
        try {
403
            $partLength = floor(strlen($token) / 10);
404
            for ($i = 0, $ct = ceil(strlen($token) / $partLength); $i < $ct; $i++) {
405
                $parts[] = substr($token, $axis, $partLength);
406
                $axis += $partLength;
407
            }
408
        } catch (\Exception $e) {
409
            $partLength = 0;
410
        }
411
412
        return array($partLength, $parts);
413
    }
414
415
    /**
416
     * Extract Ts and Module from token
417
     * @param array $parts
418
     * @param int $partLength
419
     *
420
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use 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...
421
     */
422
    private static function extractTsAndMod(array &$parts, $partLength)
423
    {
424
        $ts = '';
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $ts. 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...
425
        $mod = '';
426
        foreach ($parts as &$part) {
427
            if (strlen($part) == $partLength) {
428
                $ts .= substr($part, 0, 1);
429
                $mod .= substr($part, $partLength - 2, 2);
430
                $part = substr($part, 1, $partLength - 3);
431
            }
432
        }
433
        return array($ts, $mod);
434
    }
435
436
    /**
437
     * Decode token to check authorized request
438
     * @param string $token
439
     * @param string $module
440
     *
441
     * @return null|string
442
     */
443
    private static function decodeToken($token, $module = 'PSFS')
444
    {
445
        $decoded = NULL;
446
        list($partLength, $parts) = self::extractTokenParts($token);
447
        list($ts, $mod) = self::extractTsAndMod($parts, $partLength);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $ts. 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...
448
        $hashMod = substr(strtoupper(sha1($module)), strlen($ts) / 2, strlen($ts) * 2);
449
        if (time() - (integer)$ts < 300 && $hashMod === $mod) {
450
            $decoded = implode('', $parts);
451
        }
452
        return $decoded;
453
    }
454
455
    /**
456
     * Generate a authorized token
457
     * @param string $secret
458
     * @param string $module
459
     *
460
     * @return string
461
     */
462
    public static function generateToken($secret, $module = 'PSFS')
463
    {
464
        $ts = time();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $ts. 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...
465
        $hashModule = substr(strtoupper(sha1($module)), strlen($ts) / 2, strlen($ts) * 2);
466
        $hash = hash('sha256', $secret);
467
        $insert = floor(strlen($hash) / strlen($ts));
468
        $j = 0;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $j. 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...
469
        $token = '';
470
        for ($i = 0, $ct = strlen($ts); $i < $ct; $i++) {
471
            $token .= substr($ts, $i, 1) . substr($hash, $j, $insert) . substr($hashModule, $i, 2);
472
            $j += $insert;
473
        }
474
        $token .= substr($hash, ($insert * strlen($ts)), strlen($hash) - ($insert * strlen($ts)));
475
        return $token;
476
    }
477
478
    /**
479
     * Checks if auth token is correct
480
     * @param string $token
481
     * @param string $secret
482
     * @param string $module
483
     *
484
     * @return bool
485
     */
486
    public static function checkToken($token, $secret, $module = 'PSFS')
487
    {
488
        if (0 === strlen($token) || 0 === strlen($secret)) {
489
            return false;
490
        }
491
        $module = strtolower($module);
492
        $decodedToken = self::decodeToken($token, $module);
493
        $expectedToken = self::decodeToken(self::generateToken($secret, $module), $module);
494
495
        return $decodedToken === $expectedToken;
496
    }
497
498
}
499