Completed
Push — authenticator-refactor ( 0c2983...495926 )
by Simon
06:32
created

ChangePasswordHandler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
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
    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...
33
        'changepassword',
34
        'changePasswordForm',
35
    ];
36
37
38
    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...
39
        '' => 'changepassword',
40
    ];
41
42
    /**
43
     * @param string $link The URL to recreate this request handler
44
     * @param MemberAuthenticator $authenticator
45
     */
46
    public function __construct($link, MemberAuthenticator $authenticator)
47
    {
48
        $this->link = $link;
49
        $this->authenticator = $authenticator;
50
        parent::__construct();
51
    }
52
53
    /**
54
     * Handle the change password request
55
     * @todo this could use some spring cleaning
56
     *
57
     * @return HTTPResponse|DBHTMLText
58
     */
59
    public function changepassword()
60
    {
61
        $request = $this->getRequest();
62
63
        // Extract the member from the URL.
64
        /** @var Member $member */
65
        $member = null;
66
        if ($request->getVar('m') !== null) {
67
            $member = Member::get()->filter(['ID' => (int)$request->getVar('m')])->first();
68
        }
69
        $token = $request->getVar('t');
70
71
        // Check whether we are merely changin password, or resetting.
72
        if ($token !== null && $member && $member->validateAutoLoginToken($token)) {
73
            $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...
74
75
            // Redirect to myself, but without the hash in the URL
76
            return $this->redirect($this->link);
77
        }
78
79
        if (Session::get('AutoLoginHash')) {
80
            $message = DBField::create_field(
81
                'HTMLFragment',
82
                '<p>' . _t(
83
                    'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
84
                    'Please enter a new password.'
85
                ) . '</p>'
86
            );
87
88
            // Subsequent request after the "first load with hash" (see previous if clause).
89
            return $this->buildResponse($message);
90
        }
91
92
        if (Security::getCurrentUser()) {
93
            // Logged in user requested a password change form.
94
            $message = DBField::create_field(
95
                'HTMLFragment',
96
                '<p>' . _t(
97
                    'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
98
                    'You can change your password below.'
99
                ) . '</p>'
100
            );
101
102
            return $this->buildResponse($message);
103
        }
104
        // Show a friendly message saying the login token has expired
105
        if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
106
            $customisedController = Controller::curr()->customise(
107
                array(
108
                    'Content' => DBField::create_field(
109
                        'HTMLFragment',
110
                        _t(
111
                            'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
112
                            '<p>The password reset link is invalid or expired.</p>'
113
                            . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
114
                            . ' you <a href="{link2}">logged in</a>.</p>',
115
                            [
116
                                'link1' => $this->Link('lostpassword'),
117
                                'link2' => $this->Link('login')
118
                            ]
119
                        )
120
                    )
121
                )
122
            );
123
124
            return $customisedController->renderWith('changepassword');
125
        }
126
127
        // Someone attempted to go to changepassword without token or being logged in
128
        return Security::permissionFailure(
129
            Controller::curr(),
130
            _t(
131
                'SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION',
132
                'You must be logged in in order to change your password!'
133
            )
134
        );
135
    }
136
137
    /**
138
     * @param DBField $message
139
     * @return DBHTMLText
140
     */
141
    protected function buildResponse($message)
142
    {
143
        $customisedController = Controller::curr()->customise(
144
            [
145
                'Content' => $message,
146
                'Form'    => $this->changePasswordForm()
147
            ]
148
        );
149
150
        return $customisedController->renderWith(Security::singleton()->getTemplatesFor('changepassword'));
151
    }
152
153
    /**
154
     * @param Member $member
155
     * @param string $token
156
     */
157
    protected function setSessionToken($member, $token)
158
    {
159
        // if there is a current member, they should be logged out
160
        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...
161
            /** @var LogoutHandler $handler */
162
            Injector::inst()->get(IdentityStore::class)->logOut();
163
        }
164
165
        // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
166
        Session::set('AutoLoginHash', $member->encryptWithUserSettings($token));
167
    }
168
169
    /**
170
     * Return a link to this request handler.
171
     * The link returned is supplied in the constructor
172
     * @param null $action
173
     * @return string
174
     */
175
    public function link($action = null)
176
    {
177
        if ($action) {
178
            return Controller::join_links($this->link, $action);
179
        }
180
181
        return $this->link;
182
    }
183
184
    /**
185
     * Factory method for the lost password form
186
     *
187
     * @skipUpgrade
188
     * @return ChangePasswordForm Returns the lost password form
189
     */
190
    public function changePasswordForm()
191
    {
192
        return ChangePasswordForm::create(
193
            $this,
194
            'ChangePasswordForm'
195
        );
196
    }
197
198
    /**
199
     * Change the password
200
     *
201
     * @param array $data The user submitted data
202
     * @param ChangePasswordForm $form
203
     * @return HTTPResponse
204
     */
205
    public function doChangePassword(array $data, ChangePasswordForm $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
206
    {
207
        $member = Security::getCurrentUser();
208
        // The user was logged in, check the current password
209
        if ($member && (
210
                empty($data['OldPassword']) ||
211
                !$member->checkPassword($data['OldPassword'])->isValid()
212
            )
213
        ) {
214
            $this->form->sessionMessage(
215
                _t(
216
                    'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
217
                    "Your current password does not match, please try again"
218
                ),
219
                "bad"
220
            );
221
222
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
223
            return $this->redirectBackToForm();
224
        }
225
226
        if (!$member) {
227
            if (Session::get('AutoLoginHash')) {
228
                $member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
229
            }
230
231
            // The user is not logged in and no valid auto login hash is available
232
            if (!$member) {
233
                Session::clear('AutoLoginHash');
234
235
                return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
236
            }
237
        }
238
239
        // Check the new password
240 View Code Duplication
        if (empty($data['NewPassword1'])) {
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...
241
            $this->form->sessionMessage(
242
                _t(
243
                    'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
244
                    "The new password can't be empty, please try again"
245
                ),
246
                "bad"
247
            );
248
249
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
250
            return $this->redirectBackToForm();
251
        }
252
253
        // Fail if passwords do not match
254 View Code Duplication
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
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...
255
            $this->form->sessionMessage(
256
                _t(
257
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
258
                    "You have entered your new password differently, try again"
259
                ),
260
                "bad"
261
            );
262
263
            // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
264
            return $this->redirectBackToForm();
265
        }
266
267
        // Check if the new password is accepted
268
        $validationResult = $member->changePassword($data['NewPassword1']);
269
        if (!$validationResult->isValid()) {
270
            $this->form->setSessionValidationResult($validationResult);
271
272
            return $this->redirectBackToForm();
273
        }
274
275
        // Clear locked out status
276
        $member->LockedOutUntil = null;
277
        $member->FailedLoginCount = null;
278
        // Clear the members login hashes
279
        $member->AutoLoginHash = null;
280
        $member->AutoLoginExpired = DBDatetime::create()->now();
281
        $member->write();
282
283
        if ($member->canLogIn()->isValid()) {
284
            Injector::inst()->get(IdentityStore::class)->logIn($member, false, $this->getRequest());
285
        }
286
287
        // TODO Add confirmation message to login redirect
288
        Session::clear('AutoLoginHash');
289
290
        // Redirect to backurl
291
        $backURL = $this->getBackURL();
292
        if ($backURL) {
293
            return $this->redirect($backURL);
294
        }
295
296
        // Redirect to default location - the login form saying "You are logged in as..."
297
        $url = Security::singleton()->Link('login');
298
299
        return $this->redirect($url);
300
    }
301
302
    public function redirectBackToForm()
303
    {
304
        // Redirect back to form
305
        $url = $this->addBackURLParam(CMSSecurity::singleton()->Link('changepassword'));
306
307
        return $this->redirect($url);
308
    }
309
}
310