PageForgotPassword::isProtectedPage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 0
cts 1
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca\Pages\UserAuth;
11
12
use ParagonIE\ConstantTime\Base32;
13
use DateTimeImmutable;
14
use Waca\DataObjects\Credential;
15
use Waca\DataObjects\Domain;
16
use Waca\DataObjects\User;
17
use Waca\Exceptions\ApplicationLogicException;
18
use Waca\PdoDatabase;
19
use Waca\Security\CredentialProviders\PasswordCredentialProvider;
20
use Waca\Security\EncryptionHelper;
21
use Waca\SessionAlert;
22
use Waca\Tasks\InternalPageBase;
23
use Waca\WebRequest;
24
25
class PageForgotPassword extends InternalPageBase
26
{
27
    /**
28
     * Main function for this page, when no specific actions are called.
29
     *
30
     * This is the forgotten password reset form
31
     * @category Security-Critical
32
     */
33
    protected function main()
34
    {
35
        if (WebRequest::wasPosted()) {
36
            $this->validateCSRFToken();
37
            $username = WebRequest::postString('username');
38
            $email = WebRequest::postEmail('email');
39
            $database = $this->getDatabase();
40
41
            if ($username === null || trim($username) === "" || $email === null || trim($email) === "") {
42
                throw new ApplicationLogicException("Both username and email address must be specified!");
43
            }
44
45
            $user = User::getByUsername($username, $database);
46
            $this->sendResetMail($user, $email);
47
48
            SessionAlert::success('<strong>Your password reset request has been completed.</strong> If the details you have provided match our records, you should receive an email shortly.');
49
50
            $this->redirect('login');
51
        }
52
        else {
53
            $this->assignCSRFToken();
54
            $this->setTemplate('forgot-password/forgotpw.tpl');
55
        }
56
    }
57
58
    /**
59
     * Sends a reset email if the user is authenticated
60
     *
61
     * @param User|boolean $user  The user located from the database, or false. Doesn't really matter, since we do the
62
     *                            check anyway within this method and silently skip if we don't have a user.
63
     * @param string       $email The provided email address
64
     */
65
    private function sendResetMail($user, $email)
66
    {
67
        // If the user isn't found, or the email address is wrong, skip sending the details silently.
68
        if (!$user instanceof User) {
69
            return;
70
        }
71
72
        if (strtolower($user->getEmail()) === strtolower($email)) {
73
            $clientIp = $this->getXffTrustProvider()
74
                ->getTrustedClientIp(WebRequest::remoteAddress(), WebRequest::forwardedAddress());
75
76
            $this->cleanExistingTokens($user);
77
78
            $hash = Base32::encodeUpper(openssl_random_pseudo_bytes(30));
79
80
            $encryptionHelper = new EncryptionHelper($this->getSiteConfiguration());
81
82
            $cred = new Credential();
83
            $cred->setDatabase($this->getDatabase());
84
            $cred->setFactor(-1);
85
            $cred->setUserId($user->getId());
86
            $cred->setType('reset');
87
            $cred->setData($encryptionHelper->encryptData($hash));
88
            $cred->setVersion(0);
89
            $cred->setDisabled(0);
90
            $cred->setTimeout(new DateTimeImmutable('+ 1 hour'));
91
            $cred->setPriority(9);
92
            $cred->save();
93
94
            $this->assign("user", $user);
95
            $this->assign("hash", $hash);
96
            $this->assign("remoteAddress", $clientIp);
97
98
            $emailContent = $this->fetchTemplate('forgot-password/reset-mail.tpl');
99
100
            // FIXME: domains!
101
            /** @var Domain $domain */
102
            $domain = Domain::getById(1, $this->getDatabase());
0 ignored issues
show
Unused Code introduced by
The assignment to $domain is dead and can be removed.
Loading history...
103
            $this->getEmailHelper()->sendMail(
104
                null, $user->getEmail(), "WP:ACC password reset", $emailContent);
105
        }
106
    }
107
108
    /**
109
     * Entry point for the reset action
110
     *
111
     * This is the reset password part of the form.
112
     * @category Security-Critical
113
     */
114
    protected function reset()
115
    {
116
        $si = WebRequest::getString('si');
117
        $id = WebRequest::getString('id');
118
119
        if ($si === null || trim($si) === "" || $id === null || trim($id) === "") {
120
            throw new ApplicationLogicException("Link not valid, please ensure it has copied correctly");
121
        }
122
123
        $database = $this->getDatabase();
124
        $user = $this->getResettingUser($id, $database, $si);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of Waca\Pages\UserAuth\Page...ord::getResettingUser(). ( Ignorable by Annotation )

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

124
        $user = $this->getResettingUser(/** @scrutinizer ignore-type */ $id, $database, $si);
Loading history...
125
126
        // Dual mode
127
        if (WebRequest::wasPosted()) {
128
            $this->validateCSRFToken();
129
            try {
130
                $this->doReset($user);
131
                $this->cleanExistingTokens($user);
132
            }
133
            catch (ApplicationLogicException $ex) {
134
                SessionAlert::error($ex->getMessage());
135
                $this->redirect('forgotPassword', 'reset', array('si' => $si, 'id' => $id));
136
137
                return;
138
            }
139
        }
140
        else {
141
            $this->assignCSRFToken();
142
            $this->assign('user', $user);
143
            $this->setTemplate('forgot-password/forgotpwreset.tpl');
144
            $this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
145
        }
146
    }
147
148
    /**
149
     * Gets the user resetting their password from the database, or throwing an exception if that is not possible.
150
     *
151
     * @param integer     $id       The ID of the user to retrieve
152
     * @param PdoDatabase $database The database object to use
153
     * @param string      $si       The reset hash provided
154
     *
155
     * @return User
156
     * @throws ApplicationLogicException
157
     */
158
    private function getResettingUser($id, $database, $si)
159
    {
160
        $user = User::getById($id, $database);
161
162
        if ($user === false || $user->isCommunityUser()) {
163
            throw new ApplicationLogicException("Password reset failed. Please try again.");
164
        }
165
166
        $statement = $database->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
167
        $statement->execute([':user' => $user->getId()]);
168
169
        /** @var Credential $credential */
170
        $credential = $statement->fetchObject(Credential::class);
171
172
        $statement->closeCursor();
173
174
        if ($credential === false) {
0 ignored issues
show
introduced by
The condition $credential === false is always false.
Loading history...
175
            throw new ApplicationLogicException("Password reset failed. Please try again.");
176
        }
177
178
        $credential->setDatabase($database);
179
180
        $encryptionHelper = new EncryptionHelper($this->getSiteConfiguration());
181
        if ($encryptionHelper->decryptData($credential->getData()) != $si) {
182
            throw new ApplicationLogicException("Password reset failed. Please try again.");
183
        }
184
185
        if ($credential->getTimeout() < new DateTimeImmutable()) {
186
            $credential->delete();
187
            throw new ApplicationLogicException("Password reset token expired. Please try again.");
188
        }
189
190
        return $user;
191
    }
192
193
    /**
194
     * Performs the setting of the new password
195
     *
196
     * @param User $user The user to set the password for
197
     *
198
     * @throws ApplicationLogicException
199
     */
200
    private function doReset(User $user)
201
    {
202
        $pw = WebRequest::postString('newpassword');
203
        $pw2 = WebRequest::postString('newpasswordconfirm');
204
205
        if ($pw !== $pw2) {
206
            throw new ApplicationLogicException('Passwords do not match!');
207
        }
208
209
        $passwordCredentialProvider = new PasswordCredentialProvider($user->getDatabase(), $this->getSiteConfiguration());
210
        $passwordCredentialProvider->setCredential($user, 1, $pw);
211
212
        SessionAlert::success('You may now log in!');
213
        $this->redirect('login');
214
    }
215
216
    protected function isProtectedPage()
217
    {
218
        return false;
219
    }
220
221
    /**
222
     * @param $user
223
     */
224
    private function cleanExistingTokens($user): void
225
    {
226
        // clean out existing reset tokens
227
        $statement = $this->getDatabase()->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
228
        $statement->execute([':user' => $user->getId()]);
229
        $existing = $statement->fetchAll(PdoDatabase::FETCH_CLASS, Credential::class);
230
231
        foreach ($existing as $c) {
232
            $c->setDatabase($this->getDatabase());
233
            $c->delete();
234
        }
235
    }
236
}
237