Completed
Push — master ( 2fe906...8dfca7 )
by Fran
05:27
created

Security::extractTsAndMod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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