Completed
Push — master ( da82e8...8a02a1 )
by Fran
04:13
created

Security   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 476
Duplicated Lines 1.26 %

Coupling/Cohesion

Components 2
Dependencies 6

Test Coverage

Coverage 11.05%

Importance

Changes 14
Bugs 2 Features 1
Metric Value
c 14
b 2
f 1
dl 6
loc 476
ccs 21
cts 190
cp 0.1105
rs 5.7894
wmc 65
lcom 2
cbo 6

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 7 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 getUser() 0 4 1
A getAdmin() 0 4 1
A canAccessRestrictedAdmin() 0 4 2
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

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
7
8
    /**
9
     * Class Security
10
     * @package PSFS
11
     */
12
    class Security
13
    {
14
15
        use SingletonTrait;
16
        /**
17
         * @var array $user
18
         */
19
        private $user;
20
21
        /**
22
         * @var array $admin
23
         */
24
        private $admin;
25
26
        private $authorized = FALSE;
27
28
        protected $session;
29
30
        /**
31
         * Constructor por defecto
32
         */
33 2
        public function __construct()
34
        {
35 2
            if (PHP_SESSION_NONE === session_status()) {
36 1
                session_start();
37
            }
38 2
            $this->session = (is_null($_SESSION)) ? array() : $_SESSION;
39 2
            if (NULL === $this->getSessionKey('__FLASH_CLEAR__')) {
40 2
                $this->clearFlashes();
41 2
                $this->setSessionKey('__FLASH_CLEAR__', microtime(TRUE));
42 2
            }
43 2
            $this->user = (array_key_exists(sha1('USER'), $this->session)) ? unserialize($this->session[sha1('USER')]) : NULL;
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...
44 2
            $this->admin = (array_key_exists(sha1('ADMIN'), $this->session)) ? unserialize($this->session[sha1('ADMIN')]) : NULL;
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...
45 2
        }
46
47
        /**
48
         * Método estático que devuelve los perfiles de la plataforma
49
         * @return array
50
         */
51
        public static function getProfiles()
52
        {
53
            return array(
54
                sha1('superadmin') => _('Administrador'),
55
                sha1('admin')      => _('Gestor'),
56
                sha1('user')      => _('Usuario'),
57
            );
58
        }
59
60
        /**
61
         * Method that returns all the available profiles
62
         */
63
        public function getAdminProfiles()
64
        {
65
            static::getProfiles();
66
        }
67
68
        /**
69
         * Método estático que devuelve los perfiles disponibles
70
         * @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...
71
         */
72
        public static function getCleanProfiles()
73
        {
74
            return array(
75
                '__SUPER_ADMIN__' => sha1('superadmin'),
76
                '__ADMIN__'       => sha1('admin'),
77
                '__USER__'       => sha1('user'),
78
            );
79
        }
80
81
        /**
82
         * Método estático que devuelve los perfiles disponibles
83
         * @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...
84
         */
85
        public function getAdminCleanProfiles()
86
        {
87
            return static::getCleanProfiles();
88
        }
89
90
        /**
91
         * Método que guarda los administradores
92
         *
93
         * @param array $user
94
         *
95
         * @return bool
96
         */
97
        public static function save($user)
98
        {
99
            $admins = array();
100 View Code Duplication
            if (file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
101
                $admins = json_decode(file_get_contents(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json'), TRUE);
102
            }
103
            $admins[$user['username']]['hash'] = sha1($user['username'] . $user['password']);
104
            $admins[$user['username']]['profile'] = $user['profile'];
105
106
            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 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...
107
        }
108
109
        /**
110
         * Method to save a new admin user
111
         * @param array $user
112
         * @return bool
113
         */
114
        public function saveUser($user) {
115
            $saved = false;
116
            if(!empty($user)) {
117
                $saved = static::save($user);
118
            }
119
            return $saved;
120
        }
121
122
        /**
123
         * Servicio que actualiza los datos del usuario
124
         *
125
         * @param $user
126
         */
127
        public function updateUser($user)
128
        {
129
            $this->user = $user;
130
        }
131
132
        /**
133
         * Método que devuelve los administradores de una plataforma
134
         * @return array|mixed
135
         */
136
        public function getAdmins()
137
        {
138
            $admins = array();
139 View Code Duplication
            if (file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
140
                $admins = json_decode(file_get_contents(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json'), TRUE);
141
            }
142
143
            return $admins;
144
        }
145
146
        /**
147
         * Método que devuelve si un usuario tiene privilegios para acceder a la zona de administración
148
         *
149
         * @param null $user
150
         * @param null $pass
151
         *
152
         * @return bool
153
         * @throws \HttpException
154
         */
155
        public function checkAdmin($user = NULL, $pass = NULL)
156
        {
157
            Logger::log('Checking admin session');
158
            if (!$this->authorized) {
159
                $request = Request::getInstance();
160
                if (!file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . 'admins.json')) {
161
                    //Si no hay fichero de usuarios redirigimos directamente al gestor
162
                    return Router::getInstance()->getAdmin()->adminers();
163
                }
164
                $admins = $this->getAdmins();
165
                //Sacamos las credenciales de la petición
166
                $user = $user ?: $request->getServer('PHP_AUTH_USER');
167
                $pass = $pass ?: $request->getServer('PHP_AUTH_PW');
168
                if (NULL === $user || (array_key_exists($user, $admins) && empty($admins[$user]))) {
169
                    list($user, $pass) = $this->getAdminFromCookie();
170
                }
171
                if (!empty($user) && !empty($admins[$user])) {
172
                    $auth = $admins[$user]['hash'];
173
                    $this->authorized = ($auth == sha1($user . $pass));
174
                    $this->admin = array(
175
                        'alias'   => $user,
176
                        'profile' => $admins[$user]['profile'],
177
                    );
178
                }
179
            }
180
181
            return $this->authorized;
182
        }
183
184
        /**
185
         * Método que obtiene el usuario y contraseña de la cookie de sesión de administración
186
         * @return array
187
         */
188
        protected function getAdminFromCookie()
189
        {
190
            $auth_cookie = Request::getInstance()->getCookie($this->getHash());
191
            $user = $pass = array();
192
            if (!empty($auth_cookie)) {
193
                list($user, $pass) = explode(':', base64_decode($auth_cookie));
194
            }
195
196
            return array($user, $pass);
197
        }
198
199
        /**
200
         * Método privado para la generación del hash de la cookie de administración
201
         * @return string
202
         */
203
        public function getHash()
204
        {
205
            return substr(md5('admin'), 0, 8);
206
        }
207
208
        /**
209
         * Método que devuelve el usuario logado
210
         * @return user
0 ignored issues
show
Documentation introduced by
Should the return type not be array? 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...
211
         */
212
        public function getUser()
213
        {
214
            return $this->user;
215
        }
216
217
        /**
218
         * Método que devuelve el usuario administrador logado
219
         * @return array
220
         */
221
        public function getAdmin()
222
        {
223
            return $this->admin;
224
        }
225
226
        /**
227
         * Método que calcula si se está logado o para acceder a administración
228
         * @return bool
229
         */
230
        public function canAccessRestrictedAdmin()
231
        {
232
            return $this->admin || preg_match('/^\/admin\/login/i', Request::requestUri());
233
        }
234
235
        /**
236
         * Servicio que devuelve una pantalla de error porque se necesita estar authenticado
237
         *
238
         * @param string|null $route
239
         *
240
         * @return string|null
241
         */
242
        public function notAuthorized($route)
243
        {
244
            return Template::getInstance()->render('notauthorized.html.twig', array(
245
                'route' => $route,
246
            ));
247
        }
248
249
        /**
250
         * Servicio que chequea si un usuario es super administrador o no
251
         * @return bool
252
         */
253
        public function isSuperAdmin()
254
        {
255
            $users = $this->getAdmins();
256
            $logged = $this->getAdminFromCookie();
257
            $profiles = Security::getCleanProfiles();
258
            if ($users[$logged[0]]) {
259
                $security = $users[$logged[0]]['profile'];
260
261
                return $profiles['__SUPER_ADMIN__'] === $security;
262
            }
263
264
            return FALSE;
265
        }
266
267
        /**
268
         * Servicio que devuelve un dato de sesión
269
         *
270
         * @param string $key
271
         *
272
         * @return mixed
273
         */
274 1
        public function getSessionKey($key)
275
        {
276 1
            $data = NULL;
277 1
            if (array_key_exists($key, $this->session)) {
278
                $data = $this->session[$key];
279
            }
280
281 1
            return $data;
282
        }
283
284
        /**
285
         * Servicio que setea una variable de sesión
286
         *
287
         * @param string $key
288
         * @param mixed $data
289
         *
290
         * @return Security
291
         */
292 1
        public function setSessionKey($key, $data = NULL)
293
        {
294 1
            $this->session[$key] = $data;
295
296 1
            return $this;
297
        }
298
299
        /**
300
         * Servicio que devuelve los mensajes flash de sesiones
301
         * @return mixed
302
         */
303
        public function getFlashes()
304
        {
305
            $flashes = $this->getSessionKey(sha1('FLASHES'));
306
307
            return (NULL !== $flashes) ? $flashes : array();
308
        }
309
310
        /**
311
         * Servicio que limpia los mensajes flash
312
         * @return $this
313
         */
314 1
        public function clearFlashes()
315
        {
316 1
            $this->setSessionKey(sha1('FLASHES'), NULL);
317
318 1
            return $this;
319
        }
320
321
        /**
322
         * Servicio que inserta un flash en sesión
323
         *
324
         * @param string $key
325
         * @param mixed $data
326
         */
327
        public function setFlash($key, $data = NULL)
328
        {
329
            $flashes = $this->getFlashes();
330
            if (!is_array($flashes)) {
331
                $flashes = array();
332
            }
333
            $flashes[$key] = $data;
334
            $this->setSessionKey(sha1('FLASHES'), $flashes);
335
        }
336
337
        /**
338
         * Servicio que devuelve un flash de sesión
339
         *
340
         * @param string $key
341
         *
342
         * @return mixed
343
         */
344
        public function getFlash($key)
345
        {
346
            $flashes = $this->getFlashes();
347
348
            return (NULL !== $key && array_key_exists($key, $flashes)) ? $flashes[$key] : NULL;
349
        }
350
351
        /**
352
         * Servicio que actualiza
353
         *
354
         * @param boolean $closeSession
355
         *
356
         * @return Security
357
         */
358
        public function updateSession($closeSession = FALSE)
359
        {
360
            Logger::log('Update session');
361
            $_SESSION = $this->session;
362
            $_SESSION[sha1('USER')] = serialize($this->user);
363
            $_SESSION[sha1('ADMIN')] = serialize($this->admin);
364
            if ($closeSession) {
365
                Logger::log('Close session');
366
                session_write_close();
367
            }
368
            Logger::log('Session updated');
369
            return $this;
370
        }
371
372
        /**
373
         * Servicio que limpia la sesión
374
         */
375
        public function closeSession()
376
        {
377
            session_destroy();
378
            session_regenerate_id(TRUE);
379
        }
380
381
        /**
382
         * Extract parts from token
383
         * @param string $token
384
         *
385
         * @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...
386
         */
387
        private static function extractTokenParts($token)
388
        {
389
            $axis = 0;
390
            $parts = array();
391
            try {
392
                $partLength = floor(strlen($token) / 10);
393
                for ($i = 0, $ct = ceil(strlen($token) / $partLength); $i < $ct; $i++) {
394
                    $parts[] = substr($token, $axis, $partLength);
395
                    $axis += $partLength;
396
                }
397
            } catch(\Exception $e) {
398
                $partLength = 0;
399
            }
400
401
            return array($partLength, $parts);
402
        }
403
404
        /**
405
         * Extract Ts and Module from token
406
         * @param array $parts
407
         * @param int $partLength
408
         *
409
         * @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...
410
         */
411
        private static function extractTsAndMod(array &$parts, $partLength)
412
        {
413
            $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...
414
            $mod = '';
415
            foreach ($parts as &$part) {
416
                if (strlen($part) == $partLength) {
417
                    $ts .= substr($part, 0, 1);
418
                    $mod .= substr($part, $partLength - 2, 2);
419
                    $part = substr($part, 1, $partLength - 3);
420
                }
421
            }
422
            return array($ts, $mod);
423
        }
424
425
        /**
426
         * Decode token to check authorized request
427
         * @param string $token
428
         * @param string $module
429
         *
430
         * @return null|string
431
         */
432
        private static function decodeToken($token, $module = 'PSFS')
433
        {
434
            $decoded = NULL;
435
            list($partLength, $parts) = self::extractTokenParts($token);
436
            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...
437
            $hashMod = substr(strtoupper(sha1($module)), strlen($ts) / 2, strlen($ts) * 2);
438
            if (time() - (integer)$ts < 300 && $hashMod === $mod) {
439
                $decoded = implode('', $parts);
440
            }
441
            return $decoded;
442
        }
443
444
        /**
445
         * Generate a authorized token
446
         * @param string $secret
447
         * @param string $module
448
         *
449
         * @return string
450
         */
451
        public static function generateToken($secret, $module = 'PSFS')
452
        {
453
            $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...
454
            $hashModule = substr(strtoupper(sha1($module)), strlen($ts) / 2, strlen($ts) * 2);
455
            $hash = hash('sha256', $secret);
456
            $insert = floor(strlen($hash) / strlen($ts));
457
            $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...
458
            $token = '';
459
            for ($i = 0, $ct = strlen($ts); $i < $ct; $i++) {
460
                $token .= substr($ts, $i, 1) . substr($hash, $j, $insert) . substr($hashModule, $i, 2);
461
                $j += $insert;
462
            }
463
            $token .= substr($hash, ($insert * strlen($ts)), strlen($hash) - ($insert * strlen($ts)));
464
            return $token;
465
        }
466
467
        /**
468
         * Checks if auth token is correct
469
         * @param string $token
470
         * @param string $secret
471
         * @param string $module
472
         *
473
         * @return bool
474
         */
475
        public static function checkToken($token, $secret, $module = 'PSFS')
476
        {
477
            if (0 === strlen($token) || 0 === strlen($secret)) {
478
                return false;
479
            }
480
            $module = strtolower($module);
481
            $decodedToken = self::decodeToken($token, $module);
482
            $expectedToken = self::decodeToken(self::generateToken($secret, $module), $module);
483
484
            return $decodedToken === $expectedToken;
485
        }
486
487
    }
488