LoginOperation   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 15
c 5
b 0
f 1
lcom 1
cbo 10
dl 0
loc 182
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
A lazy_get_form() 0 4 1
A get_controls() 0 8 1
C validate() 0 122 8
B process() 0 24 5
1
<?php
2
3
/*
4
 * This file is part of the Icybee package.
5
 *
6
 * (c) Olivier Laviale <[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 Icybee\Modules\Users\Operation;
13
14
use ICanBoogie\ErrorCollection;
15
use ICanBoogie\HTTP\Request;
16
use ICanBoogie\I18n;
17
use ICanBoogie\Operation;
18
use ICanBoogie\Module\ControllerBindings as ModuleBindings;
19
20
use Icybee\Binding\Core\PrototypedBindings;
21
use Icybee\Modules\Registry\MetaCollection;
22
use Icybee\Modules\Users\LoginForm;
23
use Icybee\Modules\Users\User;
24
25
/**
26
 * Log in a user.
27
 *
28
 * @property \ICanBoogie\Core|\Icybee\Binding\CoreBindings|\ICanBoogie\Binding\Mailer\CoreBindings $app
29
 * @property User $record The logged user.
30
 */
31
class LoginOperation extends Operation
32
{
33
	use PrototypedBindings, ModuleBindings;
34
35
	/**
36
	 * Adds form control.
37
	 */
38
	protected function get_controls()
39
	{
40
		return [
41
42
			self::CONTROL_FORM => true
43
44
		] + parent::get_controls();
45
	}
46
47
	/**
48
	 * Returns the "connect" form of the target module.
49
	 */
50
	protected function lazy_get_form()
51
	{
52
		return new LoginForm;
53
	}
54
55
	/**
56
	 * @inheritdoc
57
	 */
58
	protected function validate(ErrorCollection $errors)
59
	{
60
		$request = $this->request;
61
		$username = $request[User::USERNAME];
62
		$password = $request[User::PASSWORD];
63
64
		/* @var $uid int */
65
66
		$uid = $this->model
67
		->select('uid')
68
		->where('username = ? OR email = ?', $username, $username)
69
		->rc;
70
71
		if (!$uid)
72
		{
73
			$errors
74
				->add(User::USERNAME)
75
				->add(User::PASSWORD)
76
				->add_generic("Unknown username/password combination.");
77
78
			return false;
79
		}
80
81
		/* @var $user User */
82
		/* @var $metas MetaCollection */
83
84
		$user = $this->model[$uid];
85
		$metas = $user->metas;
86
87
		$now = time();
88
		$login_unlock_time = $metas['login_unlock_time'];
89
90
		if ($login_unlock_time)
91
		{
92
			if ($login_unlock_time > $now)
93
			{
94
				throw new \Exception
95
				(
96
					\ICanBoogie\format("The user account has been locked after multiple failed login attempts.
97
					An e-mail has been sent to unlock the account. Login attempts are locked until %time,
98
					unless you unlock the account using the email sent.", [
99
100
						'%count' => $metas['failed_login_count'],
101
						'%time' => I18n\format_date($login_unlock_time, 'HH:mm')
102
103
					]),
104
105
					403
106
				);
107
			}
108
109
			$metas['login_unlock_time'] = null;
110
		}
111
112
		if (!$user->verify_password($password))
113
		{
114
			$errors
115
				->add(User::USERNAME)
116
				->add(User::PASSWORD)
117
				->add_generic("Unknown username/password combination.");
118
119
			$metas['failed_login_count'] += 1;
120
			$metas['failed_login_time'] = $now;
121
122
			if ($metas['failed_login_count'] >= 10)
123
			{
124
				$token = \ICanBoogie\generate_token(40, \ICanBoogie\TOKEN_ALPHA . \ICanBoogie\TOKEN_NUMERIC);
125
126
				$metas['login_unlock_token'] = $token;
127
				$metas['login_unlock_time'] = $now + 3600;
128
129
				$until = I18n\format_date($now + 3600, 'HH:mm');
130
131
				$url = $this->app->site->url . '/api/users/unlock_login?' . http_build_query([
132
133
					'username' => $username,
134
					'token' => $token,
135
					'continue' => $request->uri
136
137
				]);
138
139
				$this->app->mail([
140
141
					'destination' => $user->email,
142
					'from' => 'no-reply@' . $request->headers['Host'],
143
					'subject' => "Your account has been locked",
144
					'body' => <<<EOT
145
You receive this message because your account has been locked.
146
147
After multiple failed login attempts your account has been locked until $until. You can use the
148
following link to unlock your account and try to login again:
149
150
<$url>
151
152
If you forgot your password, you'll be able to request a new one.
153
154
If you didn't try to login neither forgot your password, this message might be the result of an
155
attack attempt on the website. If you think this is the case, please contact its admin.
156
157
The remote address of the request was: $request->ip.
158
EOT
159
				]);
160
161
				unset($errors[User::PASSWORD]);
162
163
				$errors->add_generic("Your account has been locked, a message has been sent to your e-mail address.");
164
			}
165
166
			return false;
167
		}
168
169
		if (!$user->is_admin && !$user->is_activated)
170
		{
171
			$errors->add_generic("User %username is not activated", [ '%username' => $username ]);
172
173
			return false;
174
		}
175
176
		$this->record = $user;
177
178
		return true;
179
	}
180
181
	/**
182
	 * Logs the user with {@link User::login()} and updates its logged date.
183
	 *
184
	 * If the user uses as legacy password, its password is updated.
185
	 *
186
	 * @return bool `true` if the user is logged.
187
	 */
188
	protected function process()
189
	{
190
		$user = $this->record;
191
		$user->metas['failed_login_count'] = null;
192
		$user->metas['failed_login_time'] = null;
193
		$user->logged_at = 'now';
194
195
		if ($user->has_legacy_password_hash)
196
		{
197
			$user->password = $this->request['password'];
198
		}
199
200
		$user->save();
201
		$user->login();
202
203
		$redirect_to = ($this->request['redirect_to'] ?: $this->request['continue']) ?: null;
204
205
		if ($redirect_to)
206
		{
207
			$this->response->location = $redirect_to;
208
		}
209
210
		return true;
211
	}
212
}
213