Issues (23)

app/models/Users.php (1 issue)

Labels
Severity
1
<?php
2
3
/*
4
 * This file is part of the Ocrend Framewok 3 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\Helpers as Helper;
16
use Ocrend\Kernel\Models\Models;
17
use Ocrend\Kernel\Models\IModels;
18
use Ocrend\Kernel\Models\ModelsException;
19
use Ocrend\Kernel\Models\Traits\DBModel;
20
use Ocrend\Kernel\Router\IRouter;
21
22
/**
23
 * Modelo Users
24
 */
25
class Users extends Models implements IModels {
26
    use DBModel;
27
28
    /**
29
     * Máximos intentos de inincio de sesión de un usuario
30
     *
31
     * @var int
32
     */
33
    const MAX_ATTEMPTS = 5;
34
35
    /**
36
     * Tiempo entre máximos intentos en segundos
37
     *
38
     * @var int
39
     */
40
    const MAX_ATTEMPTS_TIME = 120; # (dos minutos)
41
42
    /**
43
     * Log de intentos recientes con la forma 'email' => (int) intentos
44
     *
45
     * @var array
46
     */
47
    private $recentAttempts = array();
48
49
    /**
50
     * Hace un set() a la sesión login_user_recentAttempts con el valor actualizado.
51
     *
52
     * @return void
53
    */
54
    private function updateSessionAttempts() {
55
        global $session;
56
57
        $session->set('login_user_recentAttempts', $this->recentAttempts);
58
    }
59
60
    /**
61
     * Revisa si las contraseñas son iguales
62
     *
63
     * @param string $pass : Contraseña sin encriptar
64
     * @param string $pass_repeat : Contraseña repetida sin encriptar
65
     *
66
     * @throws ModelsException cuando las contraseñas no coinciden
67
     */
68
    private function checkPassMatch(string $pass, string $pass_repeat) {
69
        if ($pass != $pass_repeat) {
70
            throw new ModelsException('Las contraseñas no coinciden.');
71
        }
72
    }
73
74
    /**
75
     * Verifica el email introducido, tanto el formato como su existencia en el sistema
76
     *
77
     * @param string $email: Email del usuario
78
     *
79
     * @throws ModelsException en caso de que no tenga formato válido o ya exista
80
     */
81
    private function checkEmail(string $email) {
82
        # Formato de email
83
        if (!Helper\Strings::is_email($email)) {
84
            throw new ModelsException('El email no tiene un formato válido.');
85
        }
86
        # Existencia de email
87
        $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

87
        /** @scrutinizer ignore-call */ 
88
        $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...
88
        $query = $this->db->select('id_user', 'users', null, "email='$email'", 1);
89
        if (false !== $query) {
90
            throw new ModelsException('El email introducido ya existe.');
91
        }
92
    }
93
94
    /**
95
     * Restaura los intentos de un usuario al iniciar sesión
96
     *
97
     * @param string $email: Email del usuario a restaurar
98
     *
99
     * @throws ModelsException cuando hay un error de lógica utilizando este método
100
     * @return void
101
     */
102
    private function restoreAttempts(string $email) {       
103
        if (array_key_exists($email, $this->recentAttempts)) {
104
            $this->recentAttempts[$email]['attempts'] = 0;
105
            $this->recentAttempts[$email]['time'] = null;
106
            $this->updateSessionAttempts();
107
        } else {
108
            throw new ModelsException('Error lógico');
109
        }
110
    }
111
112
    /**
113
     * Genera la sesión con el id del usuario que ha iniciado
114
     *
115
     * @param array $user_data: Arreglo con información de la base de datos, del usuario
116
     *
117
     * @return void
118
     */
119
    private function generateSession(array $user_data) {
120
        global $session, $cookie, $config;
121
        
122
        # Generar un session hash
123
        $cookie->set('session_hash', md5(time()), $config['sessions']['user_cookie']['lifetime']);
124
        
125
        # Generar la sesión del usuario
126
        $session->set($cookie->get('session_hash') . '__user_id',(int) $user_data['id_user']);
127
128
        # Generar data encriptada para prolongar la sesión
129
        if($config['sessions']['user_cookie']['enable']) {
130
            # Generar id encriptado
131
            $encrypt = Helper\Strings::ocrend_encode($user_data['id_user'], $config['sessions']['user_cookie']['key_encrypt']);
132
133
            # Generar cookies para prolongar la vida de la sesión
134
            $cookie->set('appsalt', Helper\Strings::hash($encrypt), $config['sessions']['user_cookie']['lifetime']);
135
            $cookie->set('appencrypt', $encrypt, $config['sessions']['user_cookie']['lifetime']);
136
        }
137
    }
138
139
    /**
140
     * Verifica en la base de datos, el email y contraseña ingresados por el usuario
141
     *
142
     * @param string $email: Email del usuario que intenta el login
143
     * @param string $pass: Contraseña sin encriptar del usuario que intenta el login
144
     *
145
     * @return bool true: Cuando el inicio de sesión es correcto 
146
     *              false: Cuando el inicio de sesión no es correcto
147
     */
148
    private function authentication(string $email,string $pass) : bool {
149
        $email = $this->db->scape($email);
150
        $query = $this->db->select('id_user,pass','users',null, "email='$email'",1);
151
        
152
        # Incio de sesión con éxito
153
        if(false !== $query && Helper\Strings::chash($query[0]['pass'],$pass)) {
154
155
            # Restaurar intentos
156
            $this->restoreAttempts($email);
157
158
            # Generar la sesión
159
            $this->generateSession($query[0]);
160
            return true;
161
        }
162
163
        return false;
164
    }
165
166
    /**
167
     * Establece los intentos recientes desde la variable de sesión acumulativa
168
     *
169
     * @return void
170
     */
171
    private function setDefaultAttempts() {
172
        global $session;
173
174
        if (null != $session->get('login_user_recentAttempts')) {
175
            $this->recentAttempts = $session->get('login_user_recentAttempts');
176
        }
177
    }
178
    
179
    /**
180
     * Establece el intento del usuario actual o incrementa su cantidad si ya existe
181
     *
182
     * @param string $email: Email del usuario
183
     *
184
     * @return void
185
     */
186
    private function setNewAttempt(string $email) {
187
        if (!array_key_exists($email, $this->recentAttempts)) {
188
            $this->recentAttempts[$email] = array(
189
                'attempts' => 0, # Intentos
190
                'time' => null # Tiempo 
191
            );
192
        } 
193
194
        $this->recentAttempts[$email]['attempts']++;
195
        $this->updateSessionAttempts();
196
    }
197
198
    /**
199
     * Controla la cantidad de intentos permitidos máximos por usuario, si llega al límite,
200
     * el usuario podrá seguir intentando en self::MAX_ATTEMPTS_TIME segundos.
201
     *
202
     * @param string $email: Email del usuario
203
     *
204
     * @throws ModelsException cuando ya ha excedido self::MAX_ATTEMPTS
205
     * @return void
206
     */
207
    private function maximumAttempts(string $email) {
208
        if ($this->recentAttempts[$email]['attempts'] >= self::MAX_ATTEMPTS) {
209
            
210
            # Colocar timestamp para recuperar más adelante la posibilidad de acceso
211
            if (null == $this->recentAttempts[$email]['time']) {
212
                $this->recentAttempts[$email]['time'] = time() + self::MAX_ATTEMPTS_TIME;
213
            }
214
            
215
            if (time() < $this->recentAttempts[$email]['time']) {
216
                # Setear sesión
217
                $this->updateSessionAttempts();
218
                # Lanzar excepción
219
                throw new ModelsException('Ya ha superado el límite de intentos para iniciar sesión.');
220
            } else {
221
                $this->restoreAttempts($email);
222
            }
223
        }
224
    }   
225
226
    /**
227
     * Obtiene datos de un usuario según su id en la base de datos
228
     *    
229
     * @param int $id: Id del usuario a obtener
230
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
231
     *
232
     * @return false|array con información del usuario
233
     */   
234
    public function getUserById(int $id, string $select = '*') {
235
        return $this->db->select($select,'users',null,"id_user='$id'",1);
236
    }
237
    
238
    /**
239
     * Obtiene a todos los usuarios
240
     *    
241
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios 
242
     *
243
     * @return false|array con información de los usuarios
244
     */  
245
    public function getUsers(string $select = '*') {
246
        return $this->db->select($select, 'users');
247
    }
248
249
    /**
250
     * Obtiene datos del usuario conectado actualmente
251
     *
252
     * @param string $select : Por defecto es *, se usa para obtener sólo los parámetros necesarios
253
     *
254
     * @throws ModelsException si el usuario no está logeado
255
     * @return array con datos del usuario conectado
256
     */
257
    public function getOwnerUser(string $select = '*') : array {
258
        if(null !== $this->id_user) {    
259
               
260
            $user = $this->db->select($select,'users',null, "id_user='$this->id_user'",1);
261
262
            # Si se borra al usuario desde la base de datos y sigue con la sesión activa
263
            if(false === $user) {
264
                $this->logout();
265
            }
266
267
            return $user[0];
268
        } 
269
           
270
        throw new \RuntimeException('El usuario no está logeado.');
271
    }
272
273
     /**
274
     * Realiza la acción de login dentro del sistema
275
     *
276
     * @return array : Con información de éxito/falla al inicio de sesión.
277
     */
278
    public function login() : array {
279
        try {
280
            global $http;
281
282
            # Definir de nuevo el control de intentos
283
            $this->setDefaultAttempts();   
284
285
            # Obtener los datos $_POST
286
            $email = strtolower($http->request->get('email'));
287
            $pass = $http->request->get('pass');
288
289
            # Verificar que no están vacíos
290
            if (Helper\Functions::e($email, $pass)) {
291
                throw new ModelsException('Credenciales incompletas.');
292
            }
293
            
294
            # Añadir intentos
295
            $this->setNewAttempt($email);
296
        
297
            # Verificar intentos 
298
            $this->maximumAttempts($email);
299
300
            # Autentificar
301
            if ($this->authentication($email, $pass)) {
302
                return array('success' => 1, 'message' => 'Conectado con éxito.');
303
            }
304
            
305
            throw new ModelsException('Credenciales incorrectas.');
306
307
        } catch (ModelsException $e) {
308
            return array('success' => 0, 'message' => $e->getMessage());
309
        }        
310
    }
311
312
    /**
313
     * Realiza la acción de registro dentro del sistema
314
     *
315
     * @return array : Con información de éxito/falla al registrar el usuario nuevo.
316
     */
317
    public function register() : array {
318
        try {
319
            global $http;
320
321
            # Obtener los datos $_POST
322
            $name = $http->request->get('name');
323
            $email = $http->request->get('email');
324
            $pass = $http->request->get('pass');
325
            $pass_repeat = $http->request->get('pass_repeat');
326
327
            # Verificar que no están vacíos
328
            if (Helper\Functions::e($name, $email, $pass, $pass_repeat)) {
329
                throw new ModelsException('Todos los datos son necesarios');
330
            }
331
332
            # Verificar email 
333
            $this->checkEmail($email);
334
335
            # Veriricar contraseñas
336
            $this->checkPassMatch($pass, $pass_repeat);
337
338
            # Registrar al usuario
339
            $id_user = $this->db->insert('users', array(
340
                'name' => $name,
341
                'email' => $email,
342
                'pass' => Helper\Strings::hash($pass)
343
            ));
344
345
            # Iniciar sesión
346
            $this->generateSession(array(
347
                'id_user' => $id_user
348
            ));
349
350
            return array('success' => 1, 'message' => 'Registrado con éxito.');
351
        } catch (ModelsException $e) {
352
            return array('success' => 0, 'message' => $e->getMessage());
353
        }        
354
    }
355
    
356
    /**
357
      * Envía un correo electrónico al usuario que quiere recuperar la contraseña, con un token y una nueva contraseña.
358
      * Si el usuario no visita el enlace, el sistema no cambiará la contraseña.
359
      *
360
      * @return array<string,integer|string>
361
    */  
362
    public function lostpass() : array {
363
        try {
364
            global $http, $config;
365
366
            # Obtener datos $_POST
367
            $email = $http->request->get('email');
368
            
369
            # Campo lleno
370
            if (Helper\Functions::emp($email)) {
371
                throw new ModelsException('El campo email debe estar lleno.');
372
            }
373
374
            # Filtro
375
            $email = $this->db->scape($email);
376
377
            # Obtener información del usuario 
378
            $user_data = $this->db->select('id_user,name', 'users', null, "email='$email'", 1);
379
380
            # Verificar correo en base de datos 
381
            if (false === $user_data) {
382
                throw new ModelsException('El email no está registrado en el sistema.');
383
            }
384
385
            # Generar token y contraseña 
386
            $token = md5(time());
387
            $pass = uniqid();
388
            $link = $config['build']['url'] . 'lostpass?token='.$token.'&user='.$user_data[0]['id_user'];
389
390
            # Construir mensaje y enviar mensaje
391
            $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.
392
					<br />
393
					<br />
394
					Para cambiar su contraseña por <b>'. $pass .'</b> haga <a href="'. $link .'" target="_blank">clic aquí</a> o en el botón de recuperar.';
395
396
            # Enviar el correo electrónico
397
            $dest = array();
398
			$dest[$email] = $user_data[0]['name'];
399
            $email_send = Helper\Emails::send($dest,array(
400
                # Título del mensaje
401
                '{{title}}' => 'Recuperar contraseña de ' . $config['build']['name'],
402
                # Url de logo
403
                '{{url_logo}}' => $config['build']['url'],
404
                # Logo
405
                '{{logo}}' => $config['mailer']['logo'],
406
                # Contenido del mensaje
407
                '{{content}} ' => $HTML,
408
                # Url del botón
409
                '{{btn-href}}' => $link,
410
                # Texto del boton
411
                '{{btn-name}}' => 'Recuperar Contraseña',
412
                # Copyright
413
                '{{copyright}}' => '&copy; '.date('Y') .' <a href="'.$config['build']['url'].'">'.$config['build']['name'].'</a> - Todos los derechos reservados.'
414
              ),0);
415
416
            # Verificar si hubo algún problema con el envío del correo
417
            if(false === $email_send) {
418
                throw new ModelsException('No se ha podido enviar el correo electrónico.');
419
            }
420
421
            # Actualizar datos 
422
            $id_user = $user_data[0]['id_user'];
423
            $this->db->update('users',array(
424
                'tmp_pass' => Helper\Strings::hash($pass),
425
                'token' => $token
426
            ),"id_user='$id_user'",1);
427
428
            return array('success' => 1, 'message' => 'Se ha enviado un enlace a su correo electrónico.');
429
        } catch(ModelsException $e) {
430
            return array('success' => 0, 'message' => $e->getMessage());
431
        }
432
    }
433
434
    /**
435
     * Desconecta a un usuario si éste está conectado, y lo devuelve al inicio
436
     *
437
     * @return void
438
     */    
439
    public function logout() {
440
        global $session, $cookie;
441
	    
442
        $session->remove($cookie->get('session_hash') . '__user_id');
443
        foreach($cookie->all() as $name => $value) {
444
            $cookie->remove($name);
445
        }
446
447
        Helper\Functions::redir();
448
    }
449
450
    /**
451
     * Cambia la contraseña de un usuario en el sistema, luego de que éste haya solicitado cambiarla.
452
     * Luego retorna al sitio de inicio con la variable GET success=(bool)
453
     *
454
     * La URL debe tener la forma URL/lostpass?token=TOKEN&user=ID
455
     *
456
     * @return void
457
     */  
458
    public function changeTemporalPass() {
459
        global $config, $http;
460
        
461
        # Obtener los datos $_GET 
462
        $id_user = $http->query->get('user');
463
        $token = $http->query->get('token');
464
465
        $success = false;
466
        if (!Helper\Functions::emp($token) && is_numeric($id_user) && $id_user >= 1) {
467
            # Filtros a los datos
468
            $id_user = $this->db->scape($id_user);
469
            $token = $this->db->scape($token);
470
            # Ejecutar el cambio
471
            $this->db->query("UPDATE users SET pass=tmp_pass, tmp_pass=NULL, token=NULL
472
            WHERE id_user='$id_user' AND token='$token' LIMIT 1;");
473
            # Éxito
474
            $success = true;
475
        }
476
        
477
        # Devolover al sitio de inicio
478
        Helper\Functions::redir($config['build']['url'] . '?sucess=' . (int) $success);
479
    }
480
481
    /**
482
     * __construct()
483
     */
484
    public function __construct(IRouter $router = null) {
485
        parent::__construct($router);
486
		$this->startDBConexion();
487
    }
488
}