1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Backend\Modules\Authentication\Actions; |
4
|
|
|
|
5
|
|
|
use Backend\Core\Engine\Authentication as BackendAuthentication; |
6
|
|
|
use Backend\Core\Engine\Base\ActionIndex as BackendBaseActionIndex; |
7
|
|
|
use Backend\Core\Engine\Form as BackendForm; |
8
|
|
|
use Backend\Core\Language\Language as BL; |
9
|
|
|
use Backend\Core\Engine\Model as BackendModel; |
10
|
|
|
use Backend\Core\Engine\User; |
11
|
|
|
use Backend\Modules\Users\Engine\Model as BackendUsersModel; |
12
|
|
|
use Common\Mailer\Message; |
13
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
14
|
|
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* This is the index-action (default), it will display the login screen |
18
|
|
|
*/ |
19
|
|
|
class Index extends BackendBaseActionIndex |
20
|
|
|
{ |
21
|
|
|
/** |
22
|
|
|
* @var BackendForm |
23
|
|
|
*/ |
24
|
|
|
private $form; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var BackendForm |
28
|
|
|
*/ |
29
|
|
|
private $formForgotPassword; |
30
|
|
|
|
31
|
67 |
|
public function execute(): void |
32
|
|
|
{ |
33
|
|
|
// check if the user is really logged on |
34
|
67 |
|
if (BackendAuthentication::getUser()->isAuthenticated()) { |
35
|
1 |
|
$userEmail = BackendAuthentication::getUser()->getEmail(); |
36
|
1 |
|
$this->getContainer()->get('logger.public')->info( |
37
|
1 |
|
"User '{$userEmail}' is already authenticated." |
38
|
|
|
); |
39
|
1 |
|
$this->redirectToAllowedModuleAndAction(); |
40
|
|
|
} |
41
|
|
|
|
42
|
67 |
|
parent::execute(); |
43
|
67 |
|
$this->buildForm(); |
44
|
67 |
|
$this->validateForm(); |
45
|
67 |
|
$this->parse(); |
46
|
67 |
|
$this->display(); |
47
|
67 |
|
} |
48
|
|
|
|
49
|
67 |
|
private function buildForm(): void |
50
|
|
|
{ |
51
|
67 |
|
$this->form = new BackendForm(null, null, 'post', true, false); |
52
|
67 |
|
$this->form |
53
|
67 |
|
->addText('backend_email') |
54
|
67 |
|
->setAttribute('placeholder', \SpoonFilter::ucfirst(BL::lbl('Email'))) |
55
|
67 |
|
->setAttribute('type', 'email') |
56
|
|
|
->setAttribute('autocomplete', 'email') |
57
|
67 |
|
; |
58
|
67 |
|
$this->form |
59
|
67 |
|
->addPassword('backend_password') |
60
|
|
|
->setAttribute('placeholder', \SpoonFilter::ucfirst(BL::lbl('Password'))) |
61
|
|
|
->setAttribute('autocomplete', 'current-password') |
62
|
67 |
|
; |
63
|
67 |
|
|
64
|
67 |
|
$this->formForgotPassword = new BackendForm('forgotPassword'); |
65
|
|
|
$this->formForgotPassword |
66
|
67 |
|
->addText('backend_email_forgot') |
67
|
|
|
->setAttribute('autocomplete', 'email') |
68
|
67 |
|
; |
69
|
|
|
} |
70
|
|
|
|
71
|
67 |
|
public function parse(): void |
72
|
|
|
{ |
73
|
67 |
|
parent::parse(); |
74
|
67 |
|
|
75
|
67 |
|
// assign the interface language ourself, because it won't be assigned automagically |
76
|
|
|
$this->template->assign('INTERFACE_LANGUAGE', BL::getInterfaceLanguage()); |
77
|
67 |
|
|
78
|
|
|
$this->form->parse($this->template); |
79
|
67 |
|
$this->formForgotPassword->parse($this->template); |
80
|
41 |
|
} |
81
|
41 |
|
|
82
|
|
|
private function validateForm(): void |
83
|
|
|
{ |
84
|
41 |
|
if ($this->form->isSubmitted()) { |
85
|
|
|
$txtEmail = $this->form->getField('backend_email'); |
86
|
|
|
$txtPassword = $this->form->getField('backend_password'); |
87
|
|
|
|
88
|
|
|
// required fields |
89
|
|
|
if (!$txtEmail->isFilled() || !$txtPassword->isFilled()) { |
90
|
|
|
// add error |
91
|
|
|
$this->form->addError('fields required'); |
92
|
41 |
|
|
93
|
41 |
|
// show error |
94
|
|
|
$this->template->assign('hasError', true); |
95
|
|
|
} |
96
|
|
|
|
97
|
41 |
|
$this->getContainer()->get('logger.public')->info( |
98
|
|
|
"Trying to authenticate user '{$txtEmail->getValue()}'." |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
// invalid form-token? |
102
|
|
|
if ($this->form->getToken() != $this->form->getField('form_token')->getValue()) { |
103
|
41 |
|
// set a correct header, so bots understand they can't mess with us. |
104
|
|
|
throw new BadRequestHttpException(); |
105
|
|
|
} |
106
|
41 |
|
|
107
|
|
|
// get the user's id |
108
|
41 |
|
$userId = BackendUsersModel::getIdByEmail($txtEmail->getValue()); |
109
|
1 |
|
|
110
|
1 |
|
// all fields are ok? |
111
|
|
|
if ($txtEmail->isFilled() && $txtPassword->isFilled() && $this->form->getToken() == $this->form->getField('form_token')->getValue()) { |
112
|
|
|
// try to login the user |
113
|
|
|
if (!BackendAuthentication::loginUser($txtEmail->getValue(), $txtPassword->getValue())) { |
114
|
1 |
|
$this->getContainer()->get('logger.public')->info( |
115
|
|
|
"Failed authenticating user '{$txtEmail->getValue()}'." |
116
|
|
|
); |
117
|
1 |
|
|
118
|
|
|
// add error |
119
|
|
|
$this->form->addError('invalid login'); |
120
|
1 |
|
|
121
|
|
|
// store attempt in session |
122
|
|
|
$current = (int) BackendModel::getSession()->get('backend_login_attempts', 0); |
123
|
1 |
|
|
124
|
|
|
// increment and store |
125
|
|
|
BackendModel::getSession()->set('backend_login_attempts', ++$current); |
126
|
|
|
|
127
|
|
|
// save the failed login attempt in the user's settings |
128
|
1 |
|
if ($userId !== false) { |
129
|
|
|
BackendUsersModel::setSetting($userId, 'last_failed_login_attempt', time()); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
// show error |
133
|
41 |
|
$this->template->assign('hasError', true); |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// check sessions |
138
|
|
|
if (BackendModel::getSession()->get('backend_login_attempts', 0) >= 5) { |
139
|
|
|
// get previous attempt |
140
|
|
|
$previousAttempt = BackendModel::getSession()->get('backend_last_attempt', time()); |
141
|
|
|
|
142
|
|
|
// calculate timeout |
143
|
|
|
$timeout = 5 * (BackendModel::getSession()->get('backend_login_attempts') - 4); |
144
|
|
|
|
145
|
|
|
// too soon! |
146
|
|
|
if (time() < $previousAttempt + $timeout) { |
147
|
|
|
// sleep until the user can login again |
148
|
|
|
sleep($timeout); |
149
|
|
|
|
150
|
|
|
// set a correct header, so bots understand they can't mess with us. |
151
|
|
|
throw new ServiceUnavailableHttpException(); |
152
|
|
|
} |
153
|
|
|
// increment and store |
154
|
|
|
BackendModel::getSession()->set('backend_last_attempt', time()); |
155
|
|
|
|
156
|
|
|
// too many attempts |
157
|
|
|
$this->form->addError('too many attempts'); |
158
|
|
|
|
159
|
|
|
$this->getContainer()->get('logger.public')->info( |
160
|
|
|
"Too many login attempts for user '{$txtEmail->getValue()}'." |
161
|
|
|
); |
162
|
|
|
|
163
|
|
|
// show error |
164
|
41 |
|
$this->template->assign('hasTooManyAttemps', true); |
165
|
|
|
$this->template->assign('hasError', false); |
166
|
40 |
|
} |
167
|
40 |
|
|
168
|
|
|
// no errors in the form? |
169
|
|
|
if ($this->form->isCorrect()) { |
170
|
40 |
|
// cleanup sessions |
171
|
40 |
|
BackendModel::getSession()->remove('backend_login_attempts'); |
172
|
40 |
|
BackendModel::getSession()->remove('backend_last_attempt'); |
173
|
38 |
|
|
174
|
|
|
// save the login timestamp in the user's settings |
175
|
|
|
$lastLogin = BackendUsersModel::getSetting($userId, 'current_login'); |
|
|
|
|
176
|
40 |
|
BackendUsersModel::setSetting($userId, 'current_login', time()); |
|
|
|
|
177
|
40 |
|
if ($lastLogin) { |
178
|
|
|
BackendUsersModel::setSetting($userId, 'last_login', $lastLogin); |
179
|
|
|
} |
180
|
|
|
|
181
|
40 |
|
$this->getContainer()->get('logger.public')->info( |
182
|
|
|
"Successfully authenticated user '{$txtEmail->getValue()}'." |
183
|
|
|
); |
184
|
|
|
|
185
|
|
|
// redirect to the correct URL (URL the user was looking for or fallback) |
186
|
67 |
|
$this->redirectToAllowedModuleAndAction(); |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// is the form submitted |
191
|
|
|
if ($this->formForgotPassword->isSubmitted()) { |
192
|
|
|
// backend email |
193
|
|
|
$email = $this->formForgotPassword->getField('backend_email_forgot')->getValue(); |
194
|
|
|
|
195
|
|
|
// required fields |
196
|
|
|
if ($this->formForgotPassword->getField('backend_email_forgot')->isEmail(BL::err('EmailIsInvalid'))) { |
197
|
|
|
// check if there is a user with the given emailaddress |
198
|
|
|
if (!BackendUsersModel::existsEmail($email)) { |
199
|
|
|
$this->formForgotPassword->getField('backend_email_forgot')->addError(BL::err('EmailIsUnknown')); |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
// no errors in the form? |
204
|
|
|
if ($this->formForgotPassword->isCorrect()) { |
205
|
|
|
// generate the key for the reset link and fetch the user ID for this email |
206
|
|
|
$key = BackendAuthentication::getEncryptedString($email, uniqid('', true)); |
207
|
|
|
|
208
|
|
|
// insert the key and the timestamp into the user settings |
209
|
|
|
$userId = BackendUsersModel::getIdByEmail($email); |
210
|
|
|
$user = new User($userId); |
|
|
|
|
211
|
|
|
$user->setSetting('reset_password_key', $key); |
212
|
|
|
$user->setSetting('reset_password_timestamp', time()); |
213
|
|
|
|
214
|
|
|
// send e-mail to user |
215
|
|
|
$from = $this->get('fork.settings')->get('Core', 'mailer_from'); |
216
|
|
|
$replyTo = $this->get('fork.settings')->get('Core', 'mailer_reply_to'); |
217
|
|
|
$message = Message::newInstance( |
218
|
|
|
\SpoonFilter::ucfirst(BL::msg('ResetYourPasswordMailSubject')) |
219
|
|
|
) |
220
|
|
|
->setFrom([$from['email'] => $from['name']]) |
221
|
|
|
->setTo([$email]) |
222
|
|
|
->setReplyTo([$replyTo['email'] => $replyTo['name']]) |
223
|
|
|
->parseHtml( |
224
|
|
|
'/Authentication/Layout/Templates/Mails/ResetPassword.html.twig', |
225
|
|
|
[ |
226
|
|
|
'resetLink' => SITE_URL . BackendModel::createUrlForAction('ResetPassword') |
227
|
|
|
. '&email=' . $email . '&key=' . $key, |
228
|
|
|
] |
229
|
|
|
); |
230
|
|
|
$this->get('mailer')->send($message); |
231
|
|
|
|
232
|
|
|
// clear post-values |
233
|
|
|
$_POST['backend_email_forgot'] = ''; |
234
|
|
|
|
235
|
|
|
// show success message |
236
|
|
|
$this->template->assign('isForgotPasswordSuccess', true); |
237
|
|
|
|
238
|
|
|
// show form |
239
|
|
|
$this->template->assign('showForm', true); |
240
|
67 |
|
} else { |
241
|
|
|
// errors? |
242
|
|
|
$this->template->assign('showForm', true); |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
40 |
|
|
247
|
|
|
/** |
248
|
40 |
|
* Find out which module and action are allowed |
249
|
40 |
|
* and send the user on his way. |
250
|
40 |
|
*/ |
251
|
40 |
|
private function redirectToAllowedModuleAndAction(): void |
252
|
40 |
|
{ |
253
|
|
|
$allowedModule = $this->getAllowedModule(); |
254
|
40 |
|
$allowedAction = $this->getAllowedAction($allowedModule); |
|
|
|
|
255
|
40 |
|
$allowedModuleActionUrl = $allowedModule !== false && $allowedAction !== false ? |
256
|
40 |
|
BackendModel::createUrlForAction($allowedAction, $allowedModule) : |
|
|
|
|
257
|
|
|
BackendModel::createUrlForAction('Index', 'Authentication'); |
258
|
|
|
|
259
|
40 |
|
$userEmail = BackendAuthentication::getUser()->getEmail(); |
260
|
40 |
|
$this->getContainer()->get('logger.public')->info( |
261
|
40 |
|
"Redirecting user '{$userEmail}' to {$allowedModuleActionUrl}." |
262
|
40 |
|
); |
263
|
|
|
|
264
|
|
|
$this->redirect( |
265
|
|
|
$this->sanitizeQueryString( |
266
|
|
|
$this->getRequest()->query->get('querystring', $allowedModuleActionUrl), |
267
|
|
|
$allowedModuleActionUrl |
268
|
|
|
) |
269
|
|
|
); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Run through the action of a certain module and find us an action(name) this user is allowed to access. |
274
|
40 |
|
* |
275
|
|
|
* @param string $module |
276
|
40 |
|
* |
277
|
39 |
|
* @return bool|string |
278
|
|
|
*/ |
279
|
1 |
|
private function getAllowedAction(string $module) |
280
|
|
|
{ |
281
|
1 |
|
if (BackendAuthentication::isAllowedAction('Index', $module)) { |
282
|
1 |
|
return 'Index'; |
283
|
|
|
} |
284
|
|
|
$allowedAction = false; |
285
|
1 |
|
|
286
|
1 |
|
$groupsRightsActions = BackendUsersModel::getModuleGroupsRightsActions( |
287
|
1 |
|
$module |
288
|
1 |
|
); |
289
|
|
|
|
290
|
1 |
|
foreach ($groupsRightsActions as $groupsRightsAction) { |
291
|
1 |
|
$isAllowedAction = BackendAuthentication::isAllowedAction( |
292
|
1 |
|
$groupsRightsAction['action'], |
293
|
|
|
$module |
294
|
|
|
); |
295
|
|
|
if ($isAllowedAction) { |
296
|
1 |
|
$allowedAction = $groupsRightsAction['action']; |
297
|
|
|
break; |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return $allowedAction; |
302
|
|
|
} |
303
|
|
|
|
304
|
40 |
|
/** |
305
|
|
|
* Run through the modules and find us a module(name) this user is allowed to access. |
306
|
|
|
* |
307
|
40 |
|
* @return bool|string |
308
|
|
|
*/ |
309
|
|
|
private function getAllowedModule() |
310
|
40 |
|
{ |
311
|
40 |
|
// create filter with modules which may not be displayed |
312
|
|
|
$filter = ['Authentication', 'Error', 'Core']; |
313
|
40 |
|
|
314
|
38 |
|
// get all modules |
315
|
|
|
$modules = array_diff(BackendModel::getModules(), $filter); |
316
|
3 |
|
$allowedModule = false; |
317
|
3 |
|
|
318
|
2 |
|
if (BackendAuthentication::isAllowedModule('Dashboard')) { |
319
|
3 |
|
$allowedModule = 'Dashboard'; |
320
|
|
|
} else { |
321
|
|
|
foreach ($modules as $module) { |
322
|
|
|
if (BackendAuthentication::isAllowedModule($module)) { |
323
|
|
|
$allowedModule = $module; |
324
|
40 |
|
break; |
325
|
|
|
} |
326
|
|
|
} |
327
|
40 |
|
} |
328
|
|
|
|
329
|
|
|
return $allowedModule; |
330
|
40 |
|
} |
331
|
|
|
|
332
|
|
|
private function sanitizeQueryString(string $queryString, string $default): string |
333
|
|
|
{ |
334
|
40 |
|
if (!preg_match('/^\//', $queryString) |
335
|
|
|
|| preg_match('/^\/\//', $queryString) |
336
|
|
|
|| preg_match('/^\/[^a-zA-Z0-9.-_~]/', $queryString) |
337
|
|
|
) { |
338
|
|
|
return $default; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return filter_var($queryString, FILTER_SANITIZE_URL); |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|