Passed
Pull Request — 4 (#7399)
by Damian
09:42
created

ChangePasswordHandler::Link()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
4
namespace SilverStripe\Security\MemberAuthenticator;
5
6
use Psr\Container\NotFoundExceptionInterface;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Control\RequestHandler;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\FieldType\DBDatetime;
12
use SilverStripe\ORM\FieldType\DBField;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\Security\Authenticator;
15
use SilverStripe\Security\IdentityStore;
16
use SilverStripe\Security\Member;
17
use SilverStripe\Security\Security;
18
19
class ChangePasswordHandler extends RequestHandler
20
{
21
    /**
22
     * @var Authenticator
23
     */
24
    protected $authenticator;
25
26
    /**
27
     * Link to this handler
28
     *
29
     * @var string
30
     */
31
    protected $link = null;
32
33
    /**
34
     * @var array Allowed Actions
35
     */
36
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
37
        'changepassword',
38
        'changePasswordForm',
39
    ];
40
41
    /**
42
     * @var array URL Handlers. All should point to changepassword
43
     */
44
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
45
        '' => 'changepassword',
46
    ];
47
48
    /**
49
     * @param string $link The URL to recreate this request handler
50
     * @param MemberAuthenticator $authenticator
51
     */
52
    public function __construct($link, MemberAuthenticator $authenticator)
53
    {
54
        $this->link = $link;
55
        $this->authenticator = $authenticator;
56
        parent::__construct();
57
    }
58
59
    /**
60
     * Handle the change password request
61
     * @todo this could use some spring cleaning
62
     *
63
     * @return array|HTTPResponse
64
     */
65
    public function changepassword()
66
    {
67
        $request = $this->getRequest();
68
69
        // Extract the member from the URL.
70
        /** @var Member $member */
71
        $member = null;
72
        if ($request->getVar('m') !== null) {
73
            $member = Member::get()->filter(['ID' => (int)$request->getVar('m')])->first();
74
        }
75
        $token = $request->getVar('t');
76
77
        // Check whether we are merely changin password, or resetting.
78
        if ($token !== null && $member && $member->validateAutoLoginToken($token)) {
0 ignored issues
show
Bug introduced by
The method validateAutoLoginToken() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

78
        if ($token !== null && $member && $member->/** @scrutinizer ignore-call */ validateAutoLoginToken($token)) {
Loading history...
79
            $this->setSessionToken($member, $token);
80
81
            // Redirect to myself, but without the hash in the URL
82
            return $this->redirect($this->link);
83
        }
84
85
        $session = $this->getRequest()->getSession();
86
        if ($session->get('AutoLoginHash')) {
87
            $message = DBField::create_field(
88
                'HTMLFragment',
89
                '<p>' . _t(
90
                    'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
91
                    'Please enter a new password.'
92
                ) . '</p>'
93
            );
94
95
            // Subsequent request after the "first load with hash" (see previous if clause).
96
            return [
97
                'Content' => $message,
98
                'Form'    => $this->changePasswordForm()
99
            ];
100
        }
101
102
        if (Security::getCurrentUser()) {
103
            // Logged in user requested a password change form.
104
            $message = DBField::create_field(
105
                'HTMLFragment',
106
                '<p>' . _t(
107
                    'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
108
                    'You can change your password below.'
109
                ) . '</p>'
110
            );
111
112
            return [
113
                'Content' => $message,
114
                'Form'    => $this->changePasswordForm()
115
            ];
116
        }
117
        // Show a friendly message saying the login token has expired
118
        if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
119
            $message = DBField::create_field(
120
                'HTMLFragment',
121
                _t(
122
                    'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
123
                    '<p>The password reset link is invalid or expired.</p>'
124
                    . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
125
                    . ' you <a href="{link2}">logged in</a>.</p>',
126
                    [
127
                        'link1' => $this->Link('lostpassword'),
128
                        'link2' => $this->Link('login')
129
                    ]
130
                )
131
            );
132
133
            return [
134
                'Content' => $message,
135
            ];
136
        }
137
138
        // Someone attempted to go to changepassword without token or being logged in
139
        return Security::permissionFailure(
140
            Controller::curr(),
141
            _t(
142
                'SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION',
143
                'You must be logged in in order to change your password!'
144
            )
145
        );
146
    }
147
148
149
    /**
150
     * @param Member $member
151
     * @param string $token
152
     */
153
    protected function setSessionToken($member, $token)
154
    {
155
        // if there is a current member, they should be logged out
156
        if ($curMember = Security::getCurrentUser()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $curMember is dead and can be removed.
Loading history...
157
            /** @var LogoutHandler $handler */
158
            Injector::inst()->get(IdentityStore::class)->logOut();
159
        }
160
161
        // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
162
        $this->getRequest()->getSession()->set('AutoLoginHash', $member->encryptWithUserSettings($token));
163
    }
164
165
    /**
166
     * Return a link to this request handler.
167
     * The link returned is supplied in the constructor
168
     *
169
     * @param string|null $action
170
     * @return string
171
     */
172
    public function Link($action = null)
173
    {
174
        $link = Controller::join_links($this->link, $action);
175
        $this->extend('updateLink', $link, $action);
176
        return $link;
177
    }
178
179
    /**
180
     * Factory method for the lost password form
181
     *
182
     * @skipUpgrade
183
     * @return ChangePasswordForm Returns the lost password form
184
     */
185
    public function changePasswordForm()
186
    {
187
        return ChangePasswordForm::create(
188
            $this,
0 ignored issues
show
Bug introduced by
$this of type SilverStripe\Security\Me...r\ChangePasswordHandler is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

188
            /** @scrutinizer ignore-type */ $this,
Loading history...
189
            'ChangePasswordForm'
0 ignored issues
show
Bug introduced by
'ChangePasswordForm' of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

189
            /** @scrutinizer ignore-type */ 'ChangePasswordForm'
Loading history...
190
        );
191
    }
192
193
    /**
194
     * Change the password
195
     *
196
     * @param array $data The user submitted data
197
     * @param ChangePasswordForm $form
198
     * @return HTTPResponse
199
     * @throws ValidationException
200
     * @throws NotFoundExceptionInterface
201
     */
202
    public function doChangePassword(array $data, $form)
203
    {
204
        $member = Security::getCurrentUser();
205
        // The user was logged in, check the current password
206
        $oldPassword = isset($data['OldPassword']) ? $data['OldPassword'] : null;
207
        if ($member && !$this->checkPassword($member, $oldPassword)) {
208
            $form->sessionMessage(
209
                _t(
210
                    'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
211
                    'Your current password does not match, please try again'
212
                ),
213
                'bad'
214
            );
215
216
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
217
            return $this->redirectBackToForm();
218
        }
219
220
        $session = $this->getRequest()->getSession();
221
        if (!$member) {
222
            if ($session->get('AutoLoginHash')) {
223
                $member = Member::member_from_autologinhash($session->get('AutoLoginHash'));
224
            }
225
226
            // The user is not logged in and no valid auto login hash is available
227
            if (!$member) {
228
                $session->clear('AutoLoginHash');
229
230
                return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
231
            }
232
        }
233
234
        // Check the new password
235
        if (empty($data['NewPassword1'])) {
236
            $form->sessionMessage(
237
                _t(
238
                    'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
239
                    "The new password can't be empty, please try again"
240
                ),
241
                'bad'
242
            );
243
244
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
245
            return $this->redirectBackToForm();
246
        }
247
248
        // Fail if passwords do not match
249
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
250
            $form->sessionMessage(
251
                _t(
252
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
253
                    'You have entered your new password differently, try again'
254
                ),
255
                'bad'
256
            );
257
258
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
259
            return $this->redirectBackToForm();
260
        }
261
262
        // Check if the new password is accepted
263
        $validationResult = $member->changePassword($data['NewPassword1']);
264
        if (!$validationResult->isValid()) {
265
            $form->setSessionValidationResult($validationResult);
266
267
            return $this->redirectBackToForm();
268
        }
269
270
        // Clear locked out status
271
        $member->LockedOutUntil = null;
272
        $member->FailedLoginCount = null;
273
        // Clear the members login hashes
274
        $member->AutoLoginHash = null;
275
        $member->AutoLoginExpired = DBDatetime::create()->now();
276
        $member->write();
277
278
        if ($member->canLogin()) {
279
            /** @var IdentityStore $identityStore */
280
            $identityStore = Injector::inst()->get(IdentityStore::class);
281
            $identityStore->logIn($member, false, $this->getRequest());
282
        }
283
284
        $session->clear('AutoLoginHash');
285
286
        // Redirect to backurl
287
        $backURL = $this->getBackURL();
288
        if ($backURL
289
            // Don't redirect back to itself
290
            && $backURL !== Security::singleton()->Link('changepassword')
291
        ) {
292
            return $this->redirect($backURL);
293
        }
294
295
        $backURL = Security::config()->get('default_reset_password_dest');
296
        if ($backURL) {
297
            return $this->redirect($backURL);
298
        }
299
        // Redirect to default location - the login form saying "You are logged in as..."
300
        $url = Security::singleton()->Link('login');
301
302
        return $this->redirect($url);
303
    }
304
305
    /**
306
     * Something went wrong, go back to the changepassword
307
     *
308
     * @return HTTPResponse
309
     */
310
    public function redirectBackToForm()
311
    {
312
        // Redirect back to form
313
        $url = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
314
315
        return $this->redirect($url);
316
    }
317
318
    /**
319
     * Check if password is ok
320
     *
321
     * @param Member $member
322
     * @param string $password
323
     * @return bool
324
     */
325
    protected function checkPassword($member, $password)
326
    {
327
        if (empty($password)) {
328
            return false;
329
        }
330
        // With a valid user and password, check the password is correct
331
        $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
332
        foreach ($authenticators as $authenticator) {
333
            if (!$authenticator->checkPassword($member, $password)->isValid()) {
334
                return false;
335
            }
336
        }
337
        return true;
338
    }
339
}
340