Passed
Push — master ( 8f0aa9...1c3a2b )
by Brayan
02:00
created

app/models/Users.php (2 issues)

1
<?php
2
3
/*
4
 * This file is part of the Ocrend Framewok 2 package.
5
 *
6
 * (c) Ocrend Software <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
*/
11
12
namespace app\models;
13
14
use app\models as Model;
15
use Ocrend\Kernel\Models\Models;
16
use Ocrend\Kernel\Models\IModels;
17
use Ocrend\Kernel\Models\ModelsException;
18
use Ocrend\Kernel\Models\Traits\DBModel;
19
use Ocrend\Kernel\Router\IRouter;
20
use Ocrend\Kernel\Helpers\Strings;
21
use Ocrend\Kernel\Helpers\Emails;
22
23
/**
24
 * Controla todos los aspectos de un usuario dentro del sistema.
25
 *
26
 * @author Brayan Narváez <[email protected]>
27
 */
28
29
class Users extends Models implements IModels {
30
    /**
31
     * Característica para establecer conexión con base de datos. 
32
     */
33
    use DBModel;
34
35
    /**
36
     * Máximos intentos de inincio de sesión de un usuario
37
     *
38
     * @var int
39
     */
40
    const MAX_ATTEMPTS = 5;
41
42
    /**
43
     * Tiempo entre máximos intentos en segundos
44
     *
45
     * @var int
46
     */
47
    const MAX_ATTEMPTS_TIME = 120; # (dos minutos)
48
49
    /**
50
     * Log de intentos recientes con la forma 'email' => (int) intentos
51
     *
52
     * @var array
53
     */
54
    private $recentAttempts = array();
55
56
      /**
57
       * Hace un set() a la sesión login_user_recentAttempts con el valor actualizado.
58
       *
59
       * @return void
60
       */
61
    private function updateSessionAttempts() {
62
        global $session;
63
64
        $session->set('login_user_recentAttempts', $this->recentAttempts);
65
    }
66
67
    /**
68
     * Genera la sesión con el id del usuario que ha iniciado
69
     *
70
     * @param string $pass : Contraseña sin encriptar
71
     * @param string $pass_repeat : Contraseña repetida sin encriptar
72
     *
73
     * @throws ModelsException cuando las contraseñas no coinciden
74
     */
75
    private function checkPassMatch(string $pass, string $pass_repeat) {
76
        if ($pass != $pass_repeat) {
77
            throw new ModelsException('Las contraseñas no coinciden.');
78
        }
79
    }
80
81
    /**
82
     * Verifica el email introducido, tanto el formato como su existencia en el sistema
83
     *
84
     * @param string $email: Email del usuario
85
     *
86
     * @throws ModelsException en caso de que no tenga formato válido o ya exista
87
     */
88
    private function checkEmail(string $email) {
89
        # Formato de email
90
        if (!Strings::is_email($email)) {
91
            throw new ModelsException('El email no tiene un formato válido.');
92
        }
93
        # Existencia de email
94
        $email = $this->db->scape($email);
0 ignored issues
show
The method scape() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

94
        /** @scrutinizer ignore-call */ 
95
        $email = $this->db->scape($email);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
95
        $query = $this->db->select('id_user', 'users', "email='$email'", 'LIMIT 1');
96
        if (false !== $query) {
97
            throw new ModelsException('El email introducido ya existe.');
98
        }
99
    }
100
101
    /**
102
     * Restaura los intentos de un usuario al iniciar sesión
103
     *
104
     * @param string $email: Email del usuario a restaurar
105
     *
106
     * @throws ModelsException cuando hay un error de lógica utilizando este método
107
     * @return void
108
     */
109
    private function restoreAttempts(string $email) {       
110
        if (array_key_exists($email, $this->recentAttempts)) {
111
            $this->recentAttempts[$email]['attempts'] = 0;
112
            $this->recentAttempts[$email]['time'] = null;
113
            $this->updateSessionAttempts();
114
        } else {
115
            throw new ModelsException('Error lógico');
116
        }
117
       
118
    }
119
120
    /**
121
     * Genera la sesión con el id del usuario que ha iniciado
122
     *
123
     * @param array $user_data: Arreglo con información de la base de datos, del usuario
124
     *
125
     * @return void
126
     */
127
    private function generateSession(array $user_data) {
128
        global $session, $config;
129
130
        $session->set($config['sessions']['unique'] . '_user_id',(int) $user_data['id_user']);
131
    }
132
133
    /**
134
     * Verifica en la base de datos, el email y contraseña ingresados por el usuario
135
     *
136
     * @param string $email: Email del usuario que intenta el login
137
     * @param string $pass: Contraseña sin encriptar del usuario que intenta el login
138
     *
139
     * @return bool true: Cuando el inicio de sesión es correcto 
140
     *              false: Cuando el inicio de sesión no es correcto
141
     */
142
    private function authentication(string $email,string $pass) : bool {
143
        $email = $this->db->scape($email);
144
        $query = $this->db->select('id_user,pass','users',"email='$email'",'LIMIT 1');
145
        
146
        # Incio de sesión con éxito
147
        if(false !== $query && Strings::chash($query[0]['pass'],$pass)) {
148
149
            # Restaurar intentos
150
            $this->restoreAttempts($email);
151
152
            # Generar la sesión
153
            $this->generateSession($query[0]);
154
            return true;
155
        }
156
157
        return false;
158
    }
159
160
    /**
161
     * Establece los intentos recientes desde la variable de sesión acumulativa
162
     *
163
     * @return void
164
     */
165
    private function setDefaultAttempts() {
166
        global $session;
167
168
        if (null != $session->get('login_user_recentAttempts')) {
169
            $this->recentAttempts = $session->get('login_user_recentAttempts');
170
        }
171
    }
172
    
173
    /**
174
     * Establece el intento del usuario actual o incrementa su cantidad si ya existe
175
     *
176
     * @param string $email: Email del usuario
177
     *
178
     * @return void
179
     */
180
    private function setNewAttempt(string $email) {
181
        if (!array_key_exists($email, $this->recentAttempts)) {
182
            $this->recentAttempts[$email] = array(
183
                'attempts' => 0, # Intentos
184
                'time' => null # Tiempo 
185
            );
186
        } 
187
188
        $this->recentAttempts[$email]['attempts']++;
189
        $this->updateSessionAttempts();
190
    }
191
192
    /**
193
     * Controla la cantidad de intentos permitidos máximos por usuario, si llega al límite,
194
     * el usuario podrá seguir intentando en self::MAX_ATTEMPTS_TIME segundos.
195
     *
196
     * @param string $email: Email del usuario
197
     *
198
     * @throws ModelsException cuando ya ha excedido self::MAX_ATTEMPTS
199
     * @return void
200
     */
201
    private function maximumAttempts(string $email) {
202
        if ($this->recentAttempts[$email]['attempts'] >= self::MAX_ATTEMPTS) {
203
            
204
            # Colocar timestamp para recuperar más adelante la posibilidad de acceso
205
            if (null == $this->recentAttempts[$email]['time']) {
206
                $this->recentAttempts[$email]['time'] = time() + self::MAX_ATTEMPTS_TIME;
207
            }
208
            
209
            if (time() < $this->recentAttempts[$email]['time']) {
210
                # Setear sesión
211
                $this->updateSessionAttempts();
212
                # Lanzar excepción
213
                throw new ModelsException('Ya ha superado el límite de intentos para iniciar sesión.');
214
            } else {
215
                $this->restoreAttempts($email);
216
            }
217
        }
218
    }
219
220
    /**
221
     * Realiza la acción de login dentro del sistema
222
     *
223
     * @return array : Con información de éxito/falla al inicio de sesión.
224
     */
225
    public function login() : array {
226
        try {
227
            global $http;
228
229
            # Definir de nuevo el control de intentos
230
            $this->setDefaultAttempts();   
231
232
            # Obtener los datos $_POST
233
            $email = strtolower($http->request->get('email'));
234
            $pass = $http->request->get('pass');
235
236
            # Verificar que no están vacíos
237
            if ($this->functions->e($email, $pass)) {
238
                throw new ModelsException('Credenciales incompletas.');
239
            }
240
            
241
            # Añadir intentos
242
            $this->setNewAttempt($email);
243
        
244
            # Verificar intentos 
245
            $this->maximumAttempts($email);
246
247
            # Autentificar
248
            if ($this->authentication($email, $pass)) {
249
                return array('success' => 1, 'message' => 'Conectado con éxito.');
250
            }
251
            
252
            throw new ModelsException('Credenciales incorrectas.');
253
254
        } catch (ModelsException $e) {
255
            return array('success' => 0, 'message' => $e->getMessage());
256
        }        
257
    }
258
259
    /**
260
     * Realiza la acción de registro dentro del sistema
261
     *
262
     * @return array : Con información de éxito/falla al registrar el usuario nuevo.
263
     */
264
    public function register() : array {
265
        try {
266
            global $http;
267
268
            # Obtener los datos $_POST
269
            $name = $http->request->get('name');
270
            $email = $http->request->get('email');
271
            $pass = $http->request->get('pass');
272
            $pass_repeat = $http->request->get('pass_repeat');
273
274
            # Verificar que no están vacíos
275
            if ($this->functions->e($name, $email, $pass, $pass_repeat)) {
276
                throw new ModelsException('Todos los datos son necesarios');
277
            }
278
279
            # Verificar email 
280
            $this->checkEmail($email);
281
282
            # Veriricar contraseñas
283
            $this->checkPassMatch($pass, $pass_repeat);
284
285
            # Registrar al usuario
286
            $this->db->insert('users', array(
287
                'name' => $name,
288
                'email' => $email,
289
                'pass' => Strings::hash($pass)
290
            ));
291
292
            # Iniciar sesión
293
            $this->generateSession(array(
294
                'id_user' => $this->db->lastInsertId()
295
            ));
296
297
            return array('success' => 1, 'message' => 'Registrado con éxito.');
298
        } catch (ModelsException $e) {
299
            return array('success' => 0, 'message' => $e->getMessage());
300
        }        
301
    }
302
    
303
    /**
304
      * Envía un correo electrónico al usuario que quiere recuperar la contraseña, con un token y una nueva contraseña.
305
      * Si el usuario no visita el enlace, el sistema no cambiará la contraseña.
306
      *
307
      * @return array<string,integer|string>
308
    */  
309
    public function lostpass() {
310
        try {
311
            global $http, $config;
312
313
            # Obtener datos $_POST
314
            $email = $http->request->get('email');
315
            
316
            # Campo lleno
317
            if ($this->functions->emp($email)) {
318
                throw new ModelsException('El campo email debe estar lleno.');
319
            }
320
321
            # Filtro
322
            $email = $this->db->scape($email);
323
324
            # Obtener información del usuario 
325
            $user_data = $this->db->select('id_user,name', 'users', "email='$email'", 'LIMIT 1');
326
327
            # Verificar correo en base de datos 
328
            if (false === $user_data) {
329
                throw new ModelsException('El email no está registrado en el sistema.');
330
            }
331
332
            # Generar token y contraseña 
333
            $token = md5(time());
334
            $pass = uniqid();
335
336
            # Construir mensaje y enviar mensaje
337
            $HTML = 'Hola <b>'. $user_data[0]['name'] .'</b>, ha solicitado recuperar su contraseña perdida, si no ha realizado esta acción no necesita hacer nada.
338
					<br />
339
					<br />
340
					Para cambiar su contraseña por <b>'. $pass .'</b> haga <a href="'. $config['site']['url'] . 'lostpass/cambiar/&token='.$token.'&user='.$user_data[0]['id_user'].'" target="_blank">clic aquí</a>.';
341
342
            # Enviar el correo electrónico
343
            $dest = array();
344
			$dest[$email] = $user_data[0]['name'];
345
			$email = Emails::send_mail($dest,Emails::plantilla($HTML),'Recuperar contraseña perdida');
346
347
            # Verificar si hubo algún problema con el envío del correo
348
            if(false === $email) {
349
                throw new ModelsException('No se ha podido enviar el correo electrónico.');
350
            }
351
352
            # Actualizar datos 
353
            $id_user = $user_data[0]['id_user'];
354
            $this->db->update('users',array(
355
                'tmp_pass' => Strings::hash($pass),
356
                'token' => $token
357
            ),"id_user='$id_user'",'LIMIT 1');
358
359
            return array('success' => 1, 'message' => 'Se ha enviado un enlace a su correo electrónico.');
360
        } catch(ModelsException $e) {
361
            return array('success' => 0, 'message' => $e->getMessage());
362
        }
363
    }
364
365
    /**
366
     * Cambia la contraseña de un usuario en el sistema, luego de que éste haya solicitado cambiarla.
367
     * Luego retorna al sitio de inicio con la variable GET success=(bool)
368
     *
369
     * La URL debe tener la forma URL/lostpass/cambiar/&token=TOKEN&user=ID
370
     *
371
     * @return void
372
     */  
373
    public function changeTemporalPass() {
374
        global $config, $http;
375
        
376
        # Obtener los datos $_GET 
377
        $id_user = $http->query->get('user');
378
        $token = $http->query->get('token');
379
380
        if (!$this->functions->emp($token) && is_numeric($id_user) && $id_user >= 1) {
381
            # Filtros a los datos
382
            $id_user = $this->db->scape($id_user);
383
            $token = $this->db->scape($token);
384
            # Ejecutar el cambio
385
            $this->db->query("UPDATE users SET pass=tmp_pass, tmp_pass='', token=''
386
            WHERE id_user='$id_user' AND token='$token' LIMIT 1;");
387
            # Éxito
388
            $success = true;
389
        }
390
        
391
        # Devolover al sitio de inicio
392
        $this->functions->redir($config['site']['url'] . '?sucess=' . (int) isset($success));
393
    }
394
395
    /**
396
     * Desconecta a un usuario si éste está conectado, y lo devuelve al inicio
397
     *
398
     * @return void
399
     */    
400
    public function logout() {
401
        global $session, $config;
402
403 View Code Duplication
        if(null != $session->get($config['sessions']['unique'] . '_user_id')) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
            $session->remove($config['sessions']['unique'] . '_user_id');
405
        }
406
407
        $this->functions->redir();
408
    }
409
410
    /**
411
     * Obtiene datos de un usuario según su id en la base de datos
412
     *    
413
     * @param int $id: Id del usuario a obtener
414
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
415
     *
416
     * @return false|array con información del usuario
417
     */   
418
    public function getUserById(int $id, string $select = '*') {
419
        return $this->db->select($select,'users',"id_user='$id'",'LIMIT 1');
420
    }
421
    
422
    /**
423
     * Obtiene a todos los usuarios
424
     *    
425
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
426
     *
427
     * @return false|array con información de los usuarios
428
     */  
429
    public function getUsers(string $select = '*') {
430
        return $this->db->select($select,'users');
431
    }
432
433
    /**
434
     * Obtiene datos del usuario conectado actualmente
435
     *
436
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios
437
     *
438
     * @throws ModelsException si el usuario no está logeado
439
     * @return array con datos del usuario conectado
440
     */
441
    public function getOwnerUser(string $select = '*') : array {
442
        if(null !== $this->id_user) {    
443
               
444
            $user = $this->db->select($select,'users',"id_user='$this->id_user'",'LIMIT 1');
445
446
            # Si se borra al usuario desde la base de datos y sigue con la sesión activa
447
            if(false === $user) {
448
                $this->logout();
449
            }
450
451
            return $user[0];
452
        } 
453
           
454
        throw new \RuntimeException('El usuario no está logeado.');
455
    }
456
457
    /**
458
     * Instala el módulo de usuarios en la base de datos para que pueda funcionar correctamete.
459
     *
460
     * @throws \RuntimeException si no se puede realizar la query
461
     */
462
    public function install() {
463
        if (!$this->db->query("
464
            CREATE TABLE IF NOT EXISTS `users` (
465
                `id_user` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
466
                `name` varchar(100) NOT NULL,
467
                `email` varchar(150) NOT NULL,
468
                `pass` varchar(90) NOT NULL,
469
                `tmp_pass` varchar(90) NOT NULL DEFAULT '',
470
                `token` varchar(90) NOT NULL DEFAULT '',
471
                PRIMARY KEY (`id_user`)
472
            ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
473
        ")) {
474
            throw new \RuntimeException('No se ha podido instalar el módulo de usuarios.');
475
        }
476
        
477
        dump('Módulo instalado correctamente, el método <b>(new Model\Users)->install()</b> puede ser borrado.');
478
        exit(1);
479
    }
480
481
    /**
482
     * __construct()
483
     */
484
    public function __construct(IRouter $router = null) {
485
        parent::__construct($router);
486
        $this->startDBConexion();
487
    }
488
489
    /**
490
     * __destruct()
491
     */ 
492
    public function __destruct() {
493
        parent::__destruct();
494
        $this->endDBConexion();
495
    }
496
497
}
498