Passed
Push — master ( d42f9e...4f537b )
by Fran
03:16
created

Security::getHash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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