ResetPasswordAction::run()   C
last analyzed

Complexity

Conditions 15
Paths 42

Size

Total Lines 130
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 83
nc 42
nop 2
dl 0
loc 130
rs 5.1041
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Charcoal\Admin\Action\Account;
4
5
use Exception;
6
7
// From PSR-7
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
11
// From 'charcoal-admin'
12
use Charcoal\Admin\Action\AuthActionTrait;
13
use Charcoal\Admin\AdminAction;
14
use Charcoal\Admin\User;
15
use Charcoal\Admin\User\LostPasswordToken;
16
17
/**
18
 * Reset Password Action
19
 *
20
 * This action is used to process a user's new password given a valid
21
 * _password reset token_ generared by
22
 * {@see \Charcoal\Admin\Action\Account\LostPasswordAction}.
23
 *
24
 * ## Required Parameters
25
 *
26
 * - `token`
27
 * - `email`
28
 * - `password1`
29
 * - `password2`
30
 * - `g-recaptcha-response`
31
 *
32
 * ## HTTP Status Codes
33
 *
34
 * - `200` — Successful; Password has been changed
35
 * - `400` — Client error; Invalid request data
36
 * - `500` — Server error; Password could not be changed
37
 */
38
class ResetPasswordAction extends AdminAction
39
{
40
    use AuthActionTrait;
41
42
    /**
43
     * @return boolean
44
     */
45
    public function authRequired()
46
    {
47
        return false;
48
    }
49
50
    /**
51
     * Note that the lost-password action should never change status code and always return 200.
52
     *
53
     * @todo   This should be done via an Authenticator object.
54
     * @todo   Implement "sendResetPasswordEmail"
55
     * @param  RequestInterface  $request  A PSR-7 compatible Request instance.
56
     * @param  ResponseInterface $response A PSR-7 compatible Response instance.
57
     * @return ResponseInterface
58
     */
59
    public function run(RequestInterface $request, ResponseInterface $response)
60
    {
61
        $translator = $this->translator();
62
63
        $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
64
65
        $token     = $request->getParam('token');
0 ignored issues
show
Bug introduced by
The method getParam() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

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

65
        /** @scrutinizer ignore-call */ 
66
        $token     = $request->getParam('token');
Loading history...
66
        $email     = $request->getParam('email');
67
        $password1 = $request->getParam('password1');
68
        $password2 = $request->getParam('password2');
69
70
        if (!$token) {
71
            $this->addFeedback('error', $translator->translate('Missing reset token.'));
72
            $this->setSuccess(false);
73
74
            return $response->withStatus(400);
75
        }
76
77
        if (!$email) {
78
            $this->addFeedback('error', $translator->translate('Missing email.'));
79
            $this->setSuccess(false);
80
81
            return $response->withStatus(400);
82
        }
83
84
        if (!$password1) {
85
            $this->addFeedback('error', $translator->translate('Missing password'));
86
            $this->setSuccess(false);
87
88
            return $response->withStatus(400);
89
        }
90
91
        if (!$password2) {
92
            $this->addFeedback('error', $translator->translate('Missing password confirmation'));
93
            $this->setSuccess(false);
94
95
            return $response->withStatus(400);
96
        }
97
98
        if ($password1 != $password2) {
99
            $this->addFeedback('error', $translator->translate('Passwords do not match'));
100
            $this->setSuccess(false);
101
102
            return $response->withStatus(400);
103
        }
104
105
        if ($this->recaptchaEnabled() && $this->validateCaptchaFromRequest($request, $response) === false) {
106
            if ($ip) {
107
                $logMessage = sprintf(
108
                    '[Admin] Reset Password — CAPTCHA challenge failed for "%s" from %s',
109
                    $email,
110
                    $ip
111
                );
112
            } else {
113
                $logMessage = sprintf(
114
                    '[Admin] Reset Password — CAPTCHA challenge failed for "%s"',
115
                    $email
116
                );
117
            }
118
119
            $this->logger->warning($logMessage);
120
121
            return $response;
122
        }
123
124
        $failMessage = $translator->translation('An error occurred while processing the password change.');
125
126
        $user = $this->loadUser($email);
127
        if ($user === null) {
128
            if ($ip) {
129
                $logMessage = sprintf(
130
                    '[Admin] Reset Password — Can not find "%s" user in database for %s.',
131
                    $email,
132
                    $ip
133
                );
134
            } else {
135
                $logMessage = sprintf(
136
                    '[Admin] Reset Password — Can not find "%s" user in database.',
137
                    $email
138
                );
139
            }
140
            $this->logger->error($logMessage);
141
142
            $this->addFeedback('error', $failMessage);
143
            $this->setSuccess(false);
144
145
            return $response->withStatus(500);
146
        }
147
148
        if (!$this->validateToken($token, $user->id())) {
149
            $this->setFailureUrl($this->adminUrl('account/lost-password?notice=invalidtoken'));
150
            $this->addFeedback('error', $translator->translate('Your password reset token is invalid or expired.'));
151
            $this->setSuccess(false);
152
153
            return $response->withStatus(400);
154
        }
155
156
        try {
157
            $user->resetPassword($password1);
158
            $this->deleteToken($token);
159
160
            $this->addFeedback('success', $translator->translate('Your password has been successfully changed.'));
161
            $this->setSuccessUrl((string)$this->adminUrl('login?notice=newpass'));
162
            $this->setSuccess(true);
163
164
            return $response;
165
        } catch (Exception $e) {
166
            if ($ip) {
167
                $logMessage = sprintf(
168
                    '[Admin] Reset Password — Failed to process change for "%s" from %s: %s',
169
                    $email,
170
                    $ip,
171
                    $e->getMessage()
172
                );
173
            } else {
174
                $logMessage = sprintf(
175
                    '[Admin] Reset Password — Failed to process change for "%s": %s',
176
                    $email,
177
                    $e->getMessage()
178
                );
179
            }
180
            $this->logger->error($logMessage);
181
182
            $this->addFeedback('error', $failMessage);
183
            $this->setSuccess(false);
184
185
            return $response->withStatus(500);
186
        }
187
188
        return $response;
0 ignored issues
show
Unused Code introduced by
return $response is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
189
    }
190
191
    /**
192
     * @return array
193
     */
194
    public function results()
195
    {
196
        $ret = [
197
            'success'   => $this->success(),
198
            'feedbacks' => $this->feedbacks()
199
        ];
200
201
        return $ret;
202
    }
203
204
    /**
205
     * Validate the given password reset token.
206
     *
207
     * To be valid, a token should:
208
     *
209
     * - exist in the database
210
     * - not be expired
211
     * - match the given user
212
     *
213
     * @see    \Charcoal\Admin\Template\Account::validateToken()
214
     * @param  string $token  The token to validate.
215
     * @param  string $userId The user ID that should match the token.
216
     * @return boolean
217
     */
218
    private function validateToken($token, $userId)
219
    {
220
        $obj = $this->modelFactory()->create(LostPasswordToken::class);
221
        $sql = strtr('SELECT * FROM `%table` WHERE `token` = :token AND `user` = :userId AND `expiry` > NOW()', [
222
            '%table' => $obj->source()->table()
223
        ]);
224
        $obj->loadFromQuery($sql, [
225
            'token'  => $token,
226
            'userId' => $userId
227
        ]);
228
229
        return !!$obj->token();
230
    }
231
232
    /**
233
     * Delete the given password reset token.
234
     *
235
     * @param  string $token The token to delete.
236
     * @return void
237
     */
238
    private function deleteToken($token)
239
    {
240
        $obj = $this->modelFactory()->create(LostPasswordToken::class);
241
        $obj->setToken($token);
242
        $obj->delete();
243
    }
244
}
245