Completed
Pull Request — master (#7026)
by Damian
08:24
created

ChangePasswordHandler::checkPassword()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
77
78
            // Redirect to myself, but without the hash in the URL
79
            return $this->redirect($this->link);
80
        }
81
82
        if (Session::get('AutoLoginHash')) {
83
            $message = DBField::create_field(
84
                'HTMLFragment',
85
                '<p>' . _t(
86
                    'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
87
                    'Please enter a new password.'
88
                ) . '</p>'
89
            );
90
91
            // Subsequent request after the "first load with hash" (see previous if clause).
92
            return [
93
                'Content' => $message,
94
                'Form'    => $this->changePasswordForm()
95
            ];
96
        }
97
98
        if (Security::getCurrentUser()) {
99
            // Logged in user requested a password change form.
100
            $message = DBField::create_field(
101
                'HTMLFragment',
102
                '<p>' . _t(
103
                    'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
104
                    'You can change your password below.'
105
                ) . '</p>'
106
            );
107
108
            return [
109
                'Content' => $message,
110
                'Form'    => $this->changePasswordForm()
111
            ];
112
        }
113
        // Show a friendly message saying the login token has expired
114
        if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
115
            $message = [
116
                'Content' => DBField::create_field(
117
                    'HTMLFragment',
118
                    _t(
119
                        'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
120
                        '<p>The password reset link is invalid or expired.</p>'
121
                        . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
122
                        . ' you <a href="{link2}">logged in</a>.</p>',
123
                        [
124
                            'link1' => $this->link('lostpassword'),
125
                            'link2' => $this->link('login')
126
                        ]
127
                    )
128
                )
129
            ];
130
131
            return [
132
                'Content' => $message,
133
            ];
134
        }
135
136
        // Someone attempted to go to changepassword without token or being logged in
137
        return Security::permissionFailure(
138
            Controller::curr(),
139
            _t(
140
                'SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION',
141
                'You must be logged in in order to change your password!'
142
            )
143
        );
144
    }
145
146
147
    /**
148
     * @param Member $member
149
     * @param string $token
150
     */
151
    protected function setSessionToken($member, $token)
152
    {
153
        // if there is a current member, they should be logged out
154
        if ($curMember = Security::getCurrentUser()) {
155
            /** @var LogoutHandler $handler */
156
            Injector::inst()->get(IdentityStore::class)->logOut();
157
        }
158
159
        // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
160
        Session::set('AutoLoginHash', $member->encryptWithUserSettings($token));
161
    }
162
163
    /**
164
     * Return a link to this request handler.
165
     * The link returned is supplied in the constructor
166
     * @param null $action
167
     * @return string
168
     */
169
    public function link($action = null)
170
    {
171
        if ($action) {
172
            return Controller::join_links($this->link, $action);
173
        }
174
175
        return $this->link;
176
    }
177
178
    /**
179
     * Factory method for the lost password form
180
     *
181
     * @skipUpgrade
182
     * @return ChangePasswordForm Returns the lost password form
183
     */
184
    public function changePasswordForm()
185
    {
186
        return ChangePasswordForm::create(
187
            $this,
188
            'ChangePasswordForm'
189
        );
190
    }
191
192
    /**
193
     * Change the password
194
     *
195
     * @param array $data The user submitted data
196
     * @param ChangePasswordForm $form
197
     * @return HTTPResponse
198
     */
199
    public function doChangePassword(array $data, $form)
200
    {
201
        $member = Security::getCurrentUser();
202
        // The user was logged in, check the current password
203
        $oldPassword = isset($data['OldPassword']) ? $data['OldPassword'] : null;
204
        if ($member && !$this->checkPassword($member, $oldPassword)) {
205
            $form->sessionMessage(
206
                _t(
207
                    'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
208
                    'Your current password does not match, please try again'
209
                ),
210
                'bad'
211
            );
212
213
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
214
            return $this->redirectBackToForm();
215
        }
216
217
        if (!$member) {
218
            if (Session::get('AutoLoginHash')) {
219
                $member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
220
            }
221
222
            // The user is not logged in and no valid auto login hash is available
223
            if (!$member) {
224
                Session::clear('AutoLoginHash');
225
226
                return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
227
            }
228
        }
229
230
        // Check the new password
231
        if (empty($data['NewPassword1'])) {
232
            $form->sessionMessage(
233
                _t(
234
                    'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
235
                    "The new password can't be empty, please try again"
236
                ),
237
                'bad'
238
            );
239
240
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
241
            return $this->redirectBackToForm();
242
        }
243
244
        // Fail if passwords do not match
245
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
246
            $form->sessionMessage(
247
                _t(
248
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
249
                    'You have entered your new password differently, try again'
250
                ),
251
                'bad'
252
            );
253
254
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
255
            return $this->redirectBackToForm();
256
        }
257
258
        // Check if the new password is accepted
259
        $validationResult = $member->changePassword($data['NewPassword1']);
260
        if (!$validationResult->isValid()) {
261
            $form->setSessionValidationResult($validationResult);
262
263
            return $this->redirectBackToForm();
264
        }
265
266
        // Clear locked out status
267
        $member->LockedOutUntil = null;
268
        $member->FailedLoginCount = null;
269
        // Clear the members login hashes
270
        $member->AutoLoginHash = null;
271
        $member->AutoLoginExpired = DBDatetime::create()->now();
272
        $member->write();
273
274
        if ($member->canLogIn()) {
275
            /** @var IdentityStore $identityStore */
276
            $identityStore = Injector::inst()->get(IdentityStore::class);
277
            $identityStore->logIn($member, false, $this->getRequest());
278
        }
279
280
        // TODO Add confirmation message to login redirect
281
        Session::clear('AutoLoginHash');
282
283
        // Redirect to backurl
284
        $backURL = $this->getBackURL();
285
        if ($backURL) {
286
            return $this->redirect($backURL);
287
        }
288
289
        // Redirect to default location - the login form saying "You are logged in as..."
290
        $url = Security::singleton()->Link('login');
291
292
        return $this->redirect($url);
293
    }
294
295
    /**
296
     * Something went wrong, go back to the changepassword
297
     *
298
     * @return HTTPResponse
299
     */
300
    public function redirectBackToForm()
301
    {
302
        // Redirect back to form
303
        $url = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
304
305
        return $this->redirect($url);
306
    }
307
308
    /**
309
     * Check if password is ok
310
     *
311
     * @param Member $member
312
     * @param string $password
313
     * @return bool
314
     */
315
    protected function checkPassword($member, $password)
316
    {
317
        if (empty($password)) {
318
            return false;
319
        }
320
        // With a valid user and password, check the password is correct
321
        $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
322
        foreach ($authenticators as $authenticator) {
323
            if (!$authenticator->checkPassword($member, $password)->isValid()) {
324
                return false;
325
            }
326
        }
327
        return true;
328
    }
329
}
330