Issues (281)

Branch: master

Backend/Modules/Authentication/Actions/Index.php (3 issues)

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');
0 ignored issues
show
It seems like $userId can also be of type false; however, parameter $userId of Backend\Modules\Users\Engine\Model::getSetting() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

175
                $lastLogin = BackendUsersModel::getSetting(/** @scrutinizer ignore-type */ $userId, 'current_login');
Loading history...
176 40
                BackendUsersModel::setSetting($userId, 'current_login', time());
0 ignored issues
show
It seems like $userId can also be of type false; however, parameter $userId of Backend\Modules\Users\Engine\Model::setSetting() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

176
                BackendUsersModel::setSetting(/** @scrutinizer ignore-type */ $userId, 'current_login', time());
Loading history...
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);
0 ignored issues
show
It seems like $userId can also be of type false; however, parameter $userId of Backend\Core\Engine\User::__construct() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

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

210
                $user = new User(/** @scrutinizer ignore-type */ $userId);
Loading history...
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