Completed
Push — authenticator-refactor ( 082db8...732287 )
by Simon
06:42
created

ChangePasswordHandler::changePasswordForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 7
rs 9.4285
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\RequestHandler;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Control\Session;
11
use SilverStripe\ORM\FieldType\DBDatetime;
12
use SilverStripe\ORM\FieldType\DBField;
13
use SilverStripe\ORM\FieldType\DBHTMLText;
14
use SilverStripe\Security\Authenticator;
15
use SilverStripe\Security\CMSSecurity;
16
use SilverStripe\Security\Member;
17
use SilverStripe\Security\Security;
18
use SilverStripe\Security\IdentityStore;
19
20
class ChangePasswordHandler extends RequestHandler
21
{
22
    /**
23
     * @var Authenticator
24
     */
25
    protected $authenticator;
26
27
    /**
28
     * @var string
29
     */
30
    protected $link;
31
32
    /**
33
     * @var array Allowed Actions
34
     */
35
    private static $allowed_actions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
36
        'changepassword',
37
        'changePasswordForm',
38
    ];
39
40
    /**
41
     * @var array URL Handlers. All should point to changepassword
42
     */
43
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
44
        '' => 'changepassword',
45
    ];
46
47
    /**
48
     * @param string $link The URL to recreate this request handler
49
     * @param MemberAuthenticator $authenticator
50
     */
51
    public function __construct($link, MemberAuthenticator $authenticator)
52
    {
53
        $this->link = $link;
54
        $this->authenticator = $authenticator;
55
        parent::__construct();
56
    }
57
58
    /**
59
     * Handle the change password request
60
     * @todo this could use some spring cleaning
61
     *
62
     * @return array|HTTPResponse
63
     */
64
    public function changepassword()
65
    {
66
        $request = $this->getRequest();
67
68
        // Extract the member from the URL.
69
        /** @var Member $member */
70
        $member = null;
71
        if ($request->getVar('m') !== null) {
72
            $member = Member::get()->filter(['ID' => (int)$request->getVar('m')])->first();
73
        }
74
        $token = $request->getVar('t');
75
76
        // Check whether we are merely changin password, or resetting.
77
        if ($token !== null && $member && $member->validateAutoLoginToken($token)) {
78
            $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...
79
80
            // Redirect to myself, but without the hash in the URL
81
            return $this->redirect($this->link);
82
        }
83
84 View Code Duplication
        if (Session::get('AutoLoginHash')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
85
            $message = DBField::create_field(
86
                'HTMLFragment',
87
                '<p>' . _t(
88
                    'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
89
                    'Please enter a new password.'
90
                ) . '</p>'
91
            );
92
93
            // Subsequent request after the "first load with hash" (see previous if clause).
94
            return [
95
                'Content' => $message,
96
                'Form'    => $this->changePasswordForm()
97
            ];
98
        }
99
100 View Code Duplication
        if (Security::getCurrentUser()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
            // Logged in user requested a password change form.
102
            $message = DBField::create_field(
103
                'HTMLFragment',
104
                '<p>' . _t(
105
                    'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
106
                    'You can change your password below.'
107
                ) . '</p>'
108
            );
109
110
            return [
111
                'Content' => $message,
112
                'Form'    => $this->changePasswordForm()
113
            ];
114
        }
115
        // Show a friendly message saying the login token has expired
116
        if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
117
            $message = [
118
                'Content' => DBField::create_field(
119
                    'HTMLFragment',
120
                    _t(
121
                        'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
122
                        '<p>The password reset link is invalid or expired.</p>'
123
                        . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
124
                        . ' you <a href="{link2}">logged in</a>.</p>',
125
                        [
126
                            'link1' => $this->link('lostpassword'),
127
                            'link2' => $this->link('login')
128
                        ]
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
$curMember is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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
        Session::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
     * @param null $action
169
     * @return string
170
     */
171
    public function link($action = null)
172
    {
173
        if ($action) {
174
            return Controller::join_links($this->link, $action);
175
        }
176
177
        return $this->link;
178
    }
179
180
    /**
181
     * Factory method for the lost password form
182
     *
183
     * @skipUpgrade
184
     * @return ChangePasswordForm Returns the lost password form
185
     */
186
    public function changePasswordForm()
187
    {
188
        return ChangePasswordForm::create(
189
            $this,
190
            'ChangePasswordForm'
191
        );
192
    }
193
194
    /**
195
     * Change the password
196
     *
197
     * @param array $data The user submitted data
198
     * @param ChangePasswordForm $form
199
     * @return HTTPResponse
200
     */
201
    public function doChangePassword(array $data, $form)
202
    {
203
        $member = Security::getCurrentUser();
204
        // The user was logged in, check the current password
205
        if ($member && (
206
                empty($data['OldPassword']) ||
207
                !$member->checkPassword($data['OldPassword'])->isValid()
208
            )
209
        ) {
210
            $form->sessionMessage(
211
                _t(
212
                    'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
213
                    'Your current password does not match, please try again'
214
                ),
215
                'bad'
216
            );
217
218
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
219
            return $this->redirectBackToForm();
220
        }
221
222
        if (!$member) {
223
            if (Session::get('AutoLoginHash')) {
224
                $member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
225
            }
226
227
            // The user is not logged in and no valid auto login hash is available
228
            if (!$member) {
229
                Session::clear('AutoLoginHash');
230
231
                return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
232
            }
233
        }
234
235
        // Check the new password
236
        if (empty($data['NewPassword1'])) {
237
            $form->sessionMessage(
238
                _t(
239
                    'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
240
                    "The new password can't be empty, please try again"
241
                ),
242
                'bad'
243
            );
244
245
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
246
            return $this->redirectBackToForm();
247
        }
248
249
        // Fail if passwords do not match
250
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
251
            $form->sessionMessage(
252
                _t(
253
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
254
                    'You have entered your new password differently, try again'
255
                ),
256
                'bad'
257
            );
258
259
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
260
            return $this->redirectBackToForm();
261
        }
262
263
        // Check if the new password is accepted
264
        $validationResult = $member->changePassword($data['NewPassword1']);
265
        if (!$validationResult->isValid()) {
266
            $form->setSessionValidationResult($validationResult);
267
268
            return $this->redirectBackToForm();
269
        }
270
271
        // Clear locked out status
272
        $member->LockedOutUntil = null;
273
        $member->FailedLoginCount = null;
274
        // Clear the members login hashes
275
        $member->AutoLoginHash = null;
276
        $member->AutoLoginExpired = DBDatetime::create()->now();
277
        $member->write();
278
279
        if ($member->canLogIn()->isValid()) {
280
            Injector::inst()->get(IdentityStore::class)->logIn($member, false, $this->getRequest());
281
        }
282
283
        // TODO Add confirmation message to login redirect
284
        Session::clear('AutoLoginHash');
285
286
        // Redirect to backurl
287
        $backURL = $this->getBackURL();
288
        if ($backURL) {
289
            return $this->redirect($backURL);
290
        }
291
292
        // Redirect to default location - the login form saying "You are logged in as..."
293
        $url = Security::singleton()->Link('login');
294
295
        return $this->redirect($url);
296
    }
297
298
    /**
299
     * Something went wrong, go back to the changepassword
300
     *
301
     * @return HTTPResponse
302
     */
303
    public function redirectBackToForm()
304
    {
305
        // Redirect back to form
306
        $url = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
307
308
        return $this->redirect($url);
309
    }
310
}
311