Passed
Branch beta (48f425)
by Brayan
02:54
created

Users::changeTemporalPass()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 2
nop 0
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
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
Bug introduced by
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
        $email = $this->db->/** @scrutinizer ignore-call */ 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('user_id',(int) $user_data['id_user']);
131
        $session->set('unique_session', $config['sessions']['unique']);
132
    }
133
134
    /**
135
      * Verifica en la base de datos, el email y contraseña ingresados por el usuario
136
      *
137
      * @param string $email: Email del usuario que intenta el login
138
      * @param string $pass: Contraseña sin encriptar del usuario que intenta el login
139
      *
140
      * @return bool true: Cuando el inicio de sesión es correcto 
141
      *              false: Cuando el inicio de sesión no es correcto
142
    */
143
    private function authentication(string $email,string $pass) : bool {
144
        $email = $this->db->scape($email);
145
        $query = $this->db->select('id_user,pass','users',"email='$email'",'LIMIT 1');
146
        
147
        # Incio de sesión con éxito
148
        if(false !== $query && Strings::chash($query[0]['pass'],$pass)) {
149
150
            # Restaurar intentos
151
            $this->restoreAttempts($email);
152
153
            # Generar la sesión
154
            $this->generateSession($query[0]);
155
            return true;
156
        }
157
158
        return false;
159
    }
160
161
    /**
162
      * Establece los intentos recientes desde la variable de sesión acumulativa
163
      *
164
      * @return void
165
    */
166
    private function setDefaultAttempts() {
167
        global $session;
168
169
        if(null != $session->get('login_user_recentAttempts')) {
170
            $this->recentAttempts = $session->get('login_user_recentAttempts');
171
        }
172
    }
173
    
174
    /**
175
      * Establece el intento del usuario actual o incrementa su cantidad si ya existe
176
      *
177
      * @param string $email: Email del usuario
178
      *
179
      * @return void
180
    */
181
    private function setNewAttempt(string $email) {
182
        if(!array_key_exists($email,$this->recentAttempts)) {
183
            $this->recentAttempts[$email] = array(
184
                'attempts' => 0, # Intentos
185
                'time' => null # Tiempo 
186
            );
187
        } 
188
189
        $this->recentAttempts[$email]['attempts']++;
190
        $this->updateSessionAttempts();
191
    }
192
193
    /**
194
      * Controla la cantidad de intentos permitidos máximos por usuario, si llega al límite,
195
      * el usuario podrá seguir intentando en self::MAX_ATTEMPTS_TIME segundos.
196
      *
197
      * @param string $email: Email del usuario
198
      *
199
      * @throws ModelsException cuando ya ha excedido self::MAX_ATTEMPTS
200
      * @return void
201
    */
202
    private function maximumAttempts(string $email) {
203
        if($this->recentAttempts[$email]['attempts'] >= self::MAX_ATTEMPTS) {
204
            
205
            # Colocar timestamp para recuperar más adelante la posibilidad de acceso
206
            if(null == $this->recentAttempts[$email]['time']) {
207
                $this->recentAttempts[$email]['time'] = time() + self::MAX_ATTEMPTS_TIME;
208
            }
209
            
210
            if(time() < $this->recentAttempts[$email]['time']) {
211
                # Setear sesión
212
                $this->updateSessionAttempts();
213
                # Lanzar excepción
214
                throw new ModelsException('Ya ha superado el límite de intentos para iniciar sesión.');
215
            } else {
216
                $this->restoreAttempts($email);
217
            }
218
        }
219
    }
220
221
    /**
222
      * Realiza la acción de login dentro del sistema
223
      *
224
      * @return array : Con información de éxito/falla al inicio de sesión.
225
    */
226
    public function login() : array {
227
        try {
228
            global $http;
229
230
            # Definir de nuevo el control de intentos
231
            $this->setDefaultAttempts();   
232
233
            # Obtener los datos $_POST
234
            $email = strtolower($http->request->get('email'));
235
            $pass = $http->request->get('pass');
236
237
            # Verificar que no están vacíos
238
            if($this->functions->e($email,$pass)) {
239
                throw new ModelsException('Credenciales incompletas.');
240
            }
241
            
242
            # Añadir intentos
243
            $this->setNewAttempt($email);
244
        
245
            # Verificar intentos 
246
            $this->maximumAttempts($email);
247
248
            # Autentificar
249
            if($this->authentication($email,$pass)) {
250
                return array('success' => 1, 'message' => 'Conectado con éxito.');
251
            }
252
            
253
            throw new ModelsException('Credenciales incorrectas.');
254
255
        } catch(ModelsException $e) {
256
            return array('success' => 0, 'message' => $e->getMessage());
257
        }        
258
    }
259
260
    /**
261
      * Realiza la acción de registro dentro del sistema
262
      *
263
      * @return array : Con información de éxito/falla al registrar el usuario nuevo.
264
    */
265
    public function register() : array {
266
        try {
267
            global $http;
268
269
            # Obtener los datos $_POST
270
            $name = $http->request->get('name');
271
            $email = $http->request->get('email');
272
            $pass = $http->request->get('pass');
273
            $pass_repeat = $http->request->get('pass_repeat');
274
275
            # Verificar que no están vacíos
276
            if($this->functions->e($name,$email,$pass,$pass_repeat)) {
277
                throw new ModelsException('Todos los datos son necesarios');
278
            }
279
280
            # Verificar email 
281
            $this->checkEmail($email);
282
283
            # Veriricar contraseñas
284
            $this->checkPassMatch($pass,$pass_repeat);
285
286
            # Registrar al usuario
287
            $this->db->insert('users',array(
288
                'name' => $name,
289
                'email' => $email,
290
                'pass' => Strings::hash($pass)
291
            ));
292
293
            # Iniciar sesión
294
            $this->generateSession(array(
295
                'id_user' => $this->db->lastInsertId()
296
            ));
297
298
            return array('success' => 1, 'message' => 'Registrado con éxito.');
299
        } catch(ModelsException $e) {
300
            return array('success' => 0, 'message' => $e->getMessage());
301
        }        
302
    }
303
    
304
    /**
305
      * Envía un correo electrónico al usuario que quiere recuperar la contraseña, con un token y una nueva contraseña.
306
      * Si el usuario no visita el enlace, el sistema no cambiará la contraseña.
307
      *
308
      * @return array<string,integer|string>
309
    */  
310
    public function lostpass() {
311
        try {
312
            global $http, $config;
313
314
            # Obtener datos $_POST
315
            $email = $http->request->get('email');
316
            
317
            # Campo lleno
318
            if($this->functions->emp($email)) {
319
                throw new ModelsException('El campo email debe estar lleno.');
320
            }
321
322
            # Filtro
323
            $email = $this->db->scape($email);
324
325
            # Obtener información del usuario 
326
            $user_data = $this->db->select('id_user,name','users',"email='$email'",'LIMIT 1');
327
328
            # Verificar correo en base de datos 
329
            if(false === $user_data) {
330
                throw new ModelsException('El email no está registrado en el sistema.');
331
            }
332
333
            # Generar token y contraseña 
334
            $token = md5(time());
335
            $pass = uniqid();
336
337
            # Construir mensaje y enviar mensaje
338
            $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.
339
					<br />
340
					<br />
341
					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>.';
342
343
            # Enviar el correo electrónico
344
            $dest = array();
345
			$dest[$email] = $user_data[0]['name'];
346
			$email = Emails::send_mail($dest,Emails::plantilla($HTML),'Recuperar contraseña perdida');
347
348
            # Verificar si hubo algún problema con el envío del correo
349
            if(false === $email) {
350
                throw new ModelsException('No se ha podido enviar el correo electrónico.');
351
            }
352
353
            # Actualizar datos 
354
            $id_user = $user_data[0]['id_user'];
355
            $this->db->update('users',array(
356
                'tmp_pass' => Strings::hash($pass),
357
                'token' => $token
358
            ),"id_user='$id_user'",'LIMIT 1');
359
360
            return array('success' => 1, 'message' => 'Se ha enviado un enlace a su correo electrónico.');
361
        } catch(ModelsException $e) {
362
            return array('success' => 0, 'message' => $e->getMessage());
363
        }
364
    }
365
366
    /**
367
      * Cambia la contraseña de un usuario en el sistema, luego de que éste haya solicitado cambiarla.
368
      * Luego retorna al sitio de inicio con la variable GET success=(bool)
369
      *
370
      * La URL debe tener la forma URL/lostpass/cambiar/&token=TOKEN&user=ID
371
      *
372
      * @return void
373
    */  
374
    public function changeTemporalPass() {
375
        global $config, $http;
376
        
377
        # Obtener los datos $_GET 
378
        $id_user = $http->query->get('user');
379
        $token = $http->query->get('token');
380
381
        if(!$this->functions->emp($token) && is_numeric($id_user) && $id_user >= 1) {
382
            # Filtros a los datos
383
            $id_user = $this->db->scape($id_user);
384
            $token = $this->db->scape($token);
385
            # Ejecutar el cambio
386
            $this->db->query("UPDATE users SET pass=tmp_pass, tmp_pass='', token=''
387
            WHERE id_user='$id_user' AND token='$token' LIMIT 1;");
388
            # Éxito
389
            $succes = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $succes is dead and can be removed.
Loading history...
390
        }
391
        
392
        # Devolover al sitio de inicio
393
        $this->functions->redir($config['site']['url'] . '?sucess=' . (int) isset($success));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $success does not exist. Did you maybe mean $succes?
Loading history...
394
    }
395
396
    /**
397
      * Desconecta a un usuario si éste está conectado, y lo devuelve al inicio
398
      *
399
      * @return void
400
    */    
401
    public function logout() {
402
        global $session;
403
404
        if(null != $session->get('user_id')) {
405
            $session->remove('user_id');
406
        }
407
408
        $this->functions->redir();
409
    }
410
411
    /**
412
      * Obtiene datos de un usuario según su id en la base de datos
413
      *    
414
      * @param int $id: Id del usuario a obtener
415
      * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
416
      *
417
      * @return false|array con información del usuario
418
    */   
419
    public function getUserById(int $id, string $select = '*') {
420
       return $this->db->select($select,'users',"id_user='$id'",'LIMIT 1');
421
    }
422
    
423
    /**
424
      * Obtiene a todos los usuarios
425
      *    
426
      * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
427
      *
428
      * @return false|array con información de los usuarios
429
    */  
430
    public function getUsers(string $select = '*') {
431
       return $this->db->select($select,'users');
432
    }
433
434
    /**
435
      * Obtiene datos del usuario conectado actualmente
436
      *
437
      * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios
438
      *
439
      * @throws ModelsException si el usuario no está logeado
440
      * @return array con datos del usuario conectado
441
    */
442
    public function getOwnerUser(string $select = '*') : array {
443
        if(null !== $this->id_user) {    
444
               
445
            $user = $this->db->select($select,'users',"id_user='$this->id_user'",'LIMIT 1');
446
447
            # Si se borra al usuario desde la base de datos y sigue con la sesión activa
448
            if(false === $user) {
449
                $this->logout();
450
            }
451
452
            return $user[0];
453
        } 
454
           
455
        throw new \RuntimeException('El usuario no está logeado.');
456
    }
457
458
    /**
459
      * Instala el módulo de usuarios en la base de datos para que pueda funcionar correctamete.
460
      *
461
      * @throws \RuntimeException si no se puede realizar la query
462
    */
463
    public function install() {
464
        if(!$this->db->query("
465
            CREATE TABLE IF NOT EXISTS `users` (
466
                `id_user` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
467
                `name` varchar(100) NOT NULL,
468
                `email` varchar(150) NOT NULL,
469
                `pass` varchar(90) NOT NULL,
470
                `tmp_pass` varchar(90) NOT NULL,
471
                `token` varchar(90) NOT NULL,
472
                PRIMARY KEY (`id_user`)
473
            ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
474
        ")) {
475
            throw new \RuntimeException('No se ha podido instalar el módulo de usuarios.');
476
        }
477
        
478
        dump('Módulo instalado correctamente, el método <b>(new Model\Users)->install()</b> puede ser borrado.');
479
        exit(1);
480
    }
481
482
    /**
483
      * __construct()
484
    */
485
    public function __construct(IRouter $router = null) {
486
        parent::__construct($router);
487
        $this->startDBConexion();
488
    }
489
490
    /**
491
      * __destruct()
492
    */ 
493
    public function __destruct() {
494
        parent::__destruct();
495
        $this->endDBConexion();
496
    }
497
498
}