Passed
Push — newinternal-releasecandidate ( fe35c3...e02730 )
by Simon
09:46
created

PageForgotPassword::cleanExistingTokens()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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