Failed Conditions
Pull Request — newinternal-releasecandidate (#548)
by Simon
29:26 queued 19:28
created
includes/Security/CredentialProviders/PasswordCredentialProvider.php 3 patches
Indentation   +109 added lines, -109 removed lines patch added patch discarded remove patch
@@ -19,123 +19,123 @@
 block discarded – undo
19 19
 
20 20
 class PasswordCredentialProvider extends CredentialProviderBase
21 21
 {
22
-    const PASSWORD_COST = 10;
23
-    const PASSWORD_ALGO = PASSWORD_BCRYPT;
24
-
25
-    public function __construct(PdoDatabase $database, SiteConfiguration $configuration)
26
-    {
27
-        parent::__construct($database, $configuration, 'password');
28
-    }
29
-
30
-    public function authenticate(User $user, $data)
31
-    {
32
-        $storedData = $this->getCredentialData($user->getId());
33
-        if($storedData === null)
34
-        {
35
-            // No available credential matching these parameters
36
-            return false;
37
-        }
38
-
39
-        if($storedData->getVersion() !== 2) {
40
-            // Non-2 versions are not supported.
41
-            return false;
42
-        }
43
-
44
-        if (!password_verify($data, $storedData->getData())) {
45
-            return false;
46
-        }
47
-
48
-        if (password_needs_rehash($storedData->getData(), self::PASSWORD_ALGO,
49
-            array('cost' => self::PASSWORD_COST))) {
50
-            try {
51
-                $this->reallySetCredential($user, $storedData->getFactor(), $data);
52
-            }
53
-            catch (OptimisticLockFailedException $e) {
54
-                // optimistic lock failed, but no biggie. We'll catch it on the next login.
55
-            }
56
-        }
57
-
58
-        $strengthTester = new Zxcvbn();
59
-        $strength = $strengthTester->passwordStrength($data, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
60
-
61
-        /*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
22
+	const PASSWORD_COST = 10;
23
+	const PASSWORD_ALGO = PASSWORD_BCRYPT;
24
+
25
+	public function __construct(PdoDatabase $database, SiteConfiguration $configuration)
26
+	{
27
+		parent::__construct($database, $configuration, 'password');
28
+	}
29
+
30
+	public function authenticate(User $user, $data)
31
+	{
32
+		$storedData = $this->getCredentialData($user->getId());
33
+		if($storedData === null)
34
+		{
35
+			// No available credential matching these parameters
36
+			return false;
37
+		}
38
+
39
+		if($storedData->getVersion() !== 2) {
40
+			// Non-2 versions are not supported.
41
+			return false;
42
+		}
43
+
44
+		if (!password_verify($data, $storedData->getData())) {
45
+			return false;
46
+		}
47
+
48
+		if (password_needs_rehash($storedData->getData(), self::PASSWORD_ALGO,
49
+			array('cost' => self::PASSWORD_COST))) {
50
+			try {
51
+				$this->reallySetCredential($user, $storedData->getFactor(), $data);
52
+			}
53
+			catch (OptimisticLockFailedException $e) {
54
+				// optimistic lock failed, but no biggie. We'll catch it on the next login.
55
+			}
56
+		}
57
+
58
+		$strengthTester = new Zxcvbn();
59
+		$strength = $strengthTester->passwordStrength($data, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
60
+
61
+		/*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
62 62
             1 is still very guessable (guesses < 10^6), an extra character on a dictionary word can score a 1
63 63
             2 is somewhat guessable (guesses < 10^8), provides some protection from unthrottled online attacks
64 64
             3 is safely unguessable (guesses < 10^10), offers moderate protection from offline slow-hash scenario
65 65
             4 is very unguessable (guesses >= 10^10) and provides strong protection from offline slow-hash scenario         */
66 66
 
67
-        if ($strength['score'] <= 1 || PasswordBlacklist::isBlacklisted($data) || mb_strlen($data) < 8) {
68
-            // prevent login for extremely weak passwords
69
-            // at this point the user has authenticated via password, so they *know* it's weak.
70
-            SessionAlert::error('Your password is too weak to permit login. Please choose the "forgotten your password" option below and set a new one.', null);
71
-            return false;
72
-        }
73
-
74
-        return true;
75
-    }
76
-
77
-    /**
78
-     * @param User   $user
79
-     * @param int    $factor
80
-     * @param string $password
81
-     *
82
-     * @throws OptimisticLockFailedException
83
-     */
84
-    private function reallySetCredential(User $user, int $factor, string $password) : void {
85
-        $storedData = $this->getCredentialData($user->getId());
86
-
87
-        if ($storedData === null) {
88
-            $storedData = $this->createNewCredential($user);
89
-        }
90
-
91
-        $storedData->setData(password_hash($password, self::PASSWORD_ALGO, array('cost' => self::PASSWORD_COST)));
92
-        $storedData->setFactor($factor);
93
-        $storedData->setVersion(2);
94
-
95
-        $storedData->save();
96
-    }
97
-
98
-    /**
99
-     * @param User   $user
100
-     * @param int    $factor
101
-     * @param string $password
102
-     *
103
-     * @throws ApplicationLogicException
104
-     * @throws OptimisticLockFailedException
105
-     */
106
-    public function setCredential(User $user, $factor, $password)
107
-    {
108
-        if (PasswordBlacklist::isBlacklisted($password)) {
109
-            throw new ApplicationLogicException("Your new password is listed in the top 100,000 passwords. Please choose a stronger one.", null);
110
-        }
111
-
112
-        $strengthTester = new Zxcvbn();
113
-        $strength = $strengthTester->passwordStrength($password, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
114
-
115
-        /*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
67
+		if ($strength['score'] <= 1 || PasswordBlacklist::isBlacklisted($data) || mb_strlen($data) < 8) {
68
+			// prevent login for extremely weak passwords
69
+			// at this point the user has authenticated via password, so they *know* it's weak.
70
+			SessionAlert::error('Your password is too weak to permit login. Please choose the "forgotten your password" option below and set a new one.', null);
71
+			return false;
72
+		}
73
+
74
+		return true;
75
+	}
76
+
77
+	/**
78
+	 * @param User   $user
79
+	 * @param int    $factor
80
+	 * @param string $password
81
+	 *
82
+	 * @throws OptimisticLockFailedException
83
+	 */
84
+	private function reallySetCredential(User $user, int $factor, string $password) : void {
85
+		$storedData = $this->getCredentialData($user->getId());
86
+
87
+		if ($storedData === null) {
88
+			$storedData = $this->createNewCredential($user);
89
+		}
90
+
91
+		$storedData->setData(password_hash($password, self::PASSWORD_ALGO, array('cost' => self::PASSWORD_COST)));
92
+		$storedData->setFactor($factor);
93
+		$storedData->setVersion(2);
94
+
95
+		$storedData->save();
96
+	}
97
+
98
+	/**
99
+	 * @param User   $user
100
+	 * @param int    $factor
101
+	 * @param string $password
102
+	 *
103
+	 * @throws ApplicationLogicException
104
+	 * @throws OptimisticLockFailedException
105
+	 */
106
+	public function setCredential(User $user, $factor, $password)
107
+	{
108
+		if (PasswordBlacklist::isBlacklisted($password)) {
109
+			throw new ApplicationLogicException("Your new password is listed in the top 100,000 passwords. Please choose a stronger one.", null);
110
+		}
111
+
112
+		$strengthTester = new Zxcvbn();
113
+		$strength = $strengthTester->passwordStrength($password, [$user->getUsername(), $user->getOnWikiName(), $user->getEmail()]);
114
+
115
+		/*  0 means the password is extremely guessable (within 10^3 guesses), dictionary words like 'password' or 'mother' score a 0
116 116
             1 is still very guessable (guesses < 10^6), an extra character on a dictionary word can score a 1
117 117
             2 is somewhat guessable (guesses < 10^8), provides some protection from unthrottled online attacks
118 118
             3 is safely unguessable (guesses < 10^10), offers moderate protection from offline slow-hash scenario
119 119
             4 is very unguessable (guesses >= 10^10) and provides strong protection from offline slow-hash scenario         */
120 120
 
121
-        if ($strength['score'] <= 2 || mb_strlen($password) < 8) {
122
-            throw new ApplicationLogicException("Your new password is too weak. Please choose a stronger one.", null);
123
-        }
124
-
125
-        if ($strength['score'] <= 3) {
126
-            SessionAlert::warning("Your new password is not as strong as it could be. Consider replacing it with a stronger password.", null);
127
-        }
128
-
129
-        $this->reallySetCredential($user, $factor, $password);
130
-    }
131
-
132
-    /**
133
-     * @param User $user
134
-     *
135
-     * @throws ApplicationLogicException
136
-     */
137
-    public function deleteCredential(User $user)
138
-    {
139
-        throw new ApplicationLogicException('Deletion of password credential is not allowed.');
140
-    }
121
+		if ($strength['score'] <= 2 || mb_strlen($password) < 8) {
122
+			throw new ApplicationLogicException("Your new password is too weak. Please choose a stronger one.", null);
123
+		}
124
+
125
+		if ($strength['score'] <= 3) {
126
+			SessionAlert::warning("Your new password is not as strong as it could be. Consider replacing it with a stronger password.", null);
127
+		}
128
+
129
+		$this->reallySetCredential($user, $factor, $password);
130
+	}
131
+
132
+	/**
133
+	 * @param User $user
134
+	 *
135
+	 * @throws ApplicationLogicException
136
+	 */
137
+	public function deleteCredential(User $user)
138
+	{
139
+		throw new ApplicationLogicException('Deletion of password credential is not allowed.');
140
+	}
141 141
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -30,13 +30,13 @@
 block discarded – undo
30 30
     public function authenticate(User $user, $data)
31 31
     {
32 32
         $storedData = $this->getCredentialData($user->getId());
33
-        if($storedData === null)
33
+        if ($storedData === null)
34 34
         {
35 35
             // No available credential matching these parameters
36 36
             return false;
37 37
         }
38 38
 
39
-        if($storedData->getVersion() !== 2) {
39
+        if ($storedData->getVersion() !== 2) {
40 40
             // Non-2 versions are not supported.
41 41
             return false;
42 42
         }
Please login to merge, or discard this patch.
Braces   +1 added lines, -2 removed lines patch added patch discarded remove patch
@@ -30,8 +30,7 @@
 block discarded – undo
30 30
     public function authenticate(User $user, $data)
31 31
     {
32 32
         $storedData = $this->getCredentialData($user->getId());
33
-        if($storedData === null)
34
-        {
33
+        if($storedData === null) {
35 34
             // No available credential matching these parameters
36 35
             return false;
37 36
         }
Please login to merge, or discard this patch.
includes/Pages/UserAuth/PageChangePassword.php 1 patch
Indentation   +57 added lines, -57 removed lines patch added patch discarded remove patch
@@ -17,71 +17,71 @@
 block discarded – undo
17 17
 
18 18
 class PageChangePassword extends InternalPageBase
19 19
 {
20
-    /**
21
-     * Main function for this page, when no specific actions are called.
22
-     * @return void
23
-     */
24
-    protected function main()
25
-    {
26
-        $this->setHtmlTitle('Change Password');
20
+	/**
21
+	 * Main function for this page, when no specific actions are called.
22
+	 * @return void
23
+	 */
24
+	protected function main()
25
+	{
26
+		$this->setHtmlTitle('Change Password');
27 27
 
28
-        if (WebRequest::wasPosted()) {
29
-            $this->validateCSRFToken();
30
-            try {
31
-                $oldPassword = WebRequest::postString('oldpassword');
32
-                $newPassword = WebRequest::postString('newpassword');
33
-                $newPasswordConfirmation = WebRequest::postString('newpasswordconfirm');
28
+		if (WebRequest::wasPosted()) {
29
+			$this->validateCSRFToken();
30
+			try {
31
+				$oldPassword = WebRequest::postString('oldpassword');
32
+				$newPassword = WebRequest::postString('newpassword');
33
+				$newPasswordConfirmation = WebRequest::postString('newpasswordconfirm');
34 34
 
35
-                $user = User::getCurrent($this->getDatabase());
36
-                if (!$user instanceof User) {
37
-                    throw new ApplicationLogicException('User not found');
38
-                }
35
+				$user = User::getCurrent($this->getDatabase());
36
+				if (!$user instanceof User) {
37
+					throw new ApplicationLogicException('User not found');
38
+				}
39 39
 
40
-                $this->validateNewPassword($oldPassword, $newPassword, $newPasswordConfirmation, $user);
40
+				$this->validateNewPassword($oldPassword, $newPassword, $newPasswordConfirmation, $user);
41 41
 
42
-                $passwordProvider = new PasswordCredentialProvider($this->getDatabase(), $this->getSiteConfiguration());
43
-                $passwordProvider->setCredential($user, 1, $newPassword);
44
-            }
45
-            catch (ApplicationLogicException $ex) {
46
-                SessionAlert::error($ex->getMessage());
47
-                $this->redirect('changePassword');
42
+				$passwordProvider = new PasswordCredentialProvider($this->getDatabase(), $this->getSiteConfiguration());
43
+				$passwordProvider->setCredential($user, 1, $newPassword);
44
+			}
45
+			catch (ApplicationLogicException $ex) {
46
+				SessionAlert::error($ex->getMessage());
47
+				$this->redirect('changePassword');
48 48
 
49
-                return;
50
-            }
49
+				return;
50
+			}
51 51
 
52
-            SessionAlert::success('Password changed successfully!');
52
+			SessionAlert::success('Password changed successfully!');
53 53
 
54
-            $this->redirect('preferences');
55
-        }
56
-        else {
57
-            $this->assignCSRFToken();
58
-            $this->setTemplate('preferences/changePassword.tpl');
59
-            $this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
60
-        }
61
-    }
54
+			$this->redirect('preferences');
55
+		}
56
+		else {
57
+			$this->assignCSRFToken();
58
+			$this->setTemplate('preferences/changePassword.tpl');
59
+			$this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
60
+		}
61
+	}
62 62
 
63
-    /**
64
-     * @param string $oldPassword
65
-     * @param string $newPassword
66
-     * @param string $newPasswordConfirmation
67
-     * @param User   $user
68
-     *
69
-     * @throws ApplicationLogicException
70
-     */
71
-    protected function validateNewPassword($oldPassword, $newPassword, $newPasswordConfirmation, User $user)
72
-    {
73
-        if ($oldPassword === null || $newPassword === null || $newPasswordConfirmation === null) {
74
-            throw new ApplicationLogicException('All three fields must be completed to change your password');
75
-        }
63
+	/**
64
+	 * @param string $oldPassword
65
+	 * @param string $newPassword
66
+	 * @param string $newPasswordConfirmation
67
+	 * @param User   $user
68
+	 *
69
+	 * @throws ApplicationLogicException
70
+	 */
71
+	protected function validateNewPassword($oldPassword, $newPassword, $newPasswordConfirmation, User $user)
72
+	{
73
+		if ($oldPassword === null || $newPassword === null || $newPasswordConfirmation === null) {
74
+			throw new ApplicationLogicException('All three fields must be completed to change your password');
75
+		}
76 76
 
77
-        if ($newPassword !== $newPasswordConfirmation) {
78
-            throw new ApplicationLogicException('Your new passwords did not match!');
79
-        }
77
+		if ($newPassword !== $newPasswordConfirmation) {
78
+			throw new ApplicationLogicException('Your new passwords did not match!');
79
+		}
80 80
 
81
-        // TODO: adapt for MFA support
82
-        $passwordProvider = new PasswordCredentialProvider($this->getDatabase(), $this->getSiteConfiguration());
83
-        if (!$passwordProvider->authenticate($user, $oldPassword)) {
84
-            throw new ApplicationLogicException('The password you entered was incorrect.');
85
-        }
86
-    }
81
+		// TODO: adapt for MFA support
82
+		$passwordProvider = new PasswordCredentialProvider($this->getDatabase(), $this->getSiteConfiguration());
83
+		if (!$passwordProvider->authenticate($user, $oldPassword)) {
84
+			throw new ApplicationLogicException('The password you entered was incorrect.');
85
+		}
86
+	}
87 87
 }
Please login to merge, or discard this patch.
includes/Pages/UserAuth/PageForgotPassword.php 1 patch
Indentation   +205 added lines, -205 removed lines patch added patch discarded remove patch
@@ -22,209 +22,209 @@
 block discarded – undo
22 22
 
23 23
 class PageForgotPassword extends InternalPageBase
24 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> If the details you have provided match our records, you should receive an email shortly.');
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::encodeUpper(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);
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
-            $this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
139
-        }
140
-    }
141
-
142
-    /**
143
-     * Gets the user resetting their password from the database, or throwing an exception if that is not possible.
144
-     *
145
-     * @param integer     $id       The ID of the user to retrieve
146
-     * @param PdoDatabase $database The database object to use
147
-     * @param string      $si       The reset hash provided
148
-     *
149
-     * @return User
150
-     * @throws ApplicationLogicException
151
-     */
152
-    private function getResettingUser($id, $database, $si)
153
-    {
154
-        $user = User::getById($id, $database);
155
-
156
-        if ($user === false ||  $user->isCommunityUser()) {
157
-            throw new ApplicationLogicException("Password reset failed. Please try again.");
158
-        }
159
-
160
-        $statement = $database->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
161
-        $statement->execute([':user' => $user->getId()]);
162
-
163
-        /** @var Credential $credential */
164
-        $credential = $statement->fetchObject(Credential::class);
165
-
166
-        $statement->closeCursor();
167
-
168
-        if ($credential === false) {
169
-            throw new ApplicationLogicException("Password reset failed. Please try again.");
170
-        }
171
-
172
-        $credential->setDatabase($database);
173
-
174
-        $encryptionHelper = new EncryptionHelper($this->getSiteConfiguration());
175
-        if ($encryptionHelper->decryptData($credential->getData()) != $si) {
176
-            throw new ApplicationLogicException("Password reset failed. Please try again.");
177
-        }
178
-
179
-        if ($credential->getTimeout() < new DateTimeImmutable()) {
180
-            $credential->delete();
181
-            throw new ApplicationLogicException("Password reset token expired. Please try again.");
182
-        }
183
-
184
-        return $user;
185
-    }
186
-
187
-    /**
188
-     * Performs the setting of the new password
189
-     *
190
-     * @param User $user The user to set the password for
191
-     *
192
-     * @throws ApplicationLogicException
193
-     */
194
-    private function doReset(User $user)
195
-    {
196
-        $pw = WebRequest::postString('pw');
197
-        $pw2 = WebRequest::postString('pw2');
198
-
199
-        if ($pw !== $pw2) {
200
-            throw new ApplicationLogicException('Passwords do not match!');
201
-        }
202
-
203
-        $passwordCredentialProvider = new PasswordCredentialProvider($user->getDatabase(), $this->getSiteConfiguration());
204
-        $passwordCredentialProvider->setCredential($user, 1, $pw);
205
-
206
-        SessionAlert::success('You may now log in!');
207
-        $this->redirect('login');
208
-    }
209
-
210
-    protected function isProtectedPage()
211
-    {
212
-        return false;
213
-    }
214
-
215
-    /**
216
-     * @param $user
217
-     */
218
-    private function cleanExistingTokens($user): void
219
-    {
220
-        // clean out existing reset tokens
221
-        $statement = $this->getDatabase()->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
222
-        $statement->execute([':user' => $user->getId()]);
223
-        $existing = $statement->fetchAll(PdoDatabase::FETCH_CLASS, Credential::class);
224
-
225
-        foreach ($existing as $c) {
226
-            $c->setDatabase($this->getDatabase());
227
-            $c->delete();
228
-        }
229
-    }
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> If the details you have provided match our records, you should receive an email shortly.');
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::encodeUpper(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);
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
+			$this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
139
+		}
140
+	}
141
+
142
+	/**
143
+	 * Gets the user resetting their password from the database, or throwing an exception if that is not possible.
144
+	 *
145
+	 * @param integer     $id       The ID of the user to retrieve
146
+	 * @param PdoDatabase $database The database object to use
147
+	 * @param string      $si       The reset hash provided
148
+	 *
149
+	 * @return User
150
+	 * @throws ApplicationLogicException
151
+	 */
152
+	private function getResettingUser($id, $database, $si)
153
+	{
154
+		$user = User::getById($id, $database);
155
+
156
+		if ($user === false ||  $user->isCommunityUser()) {
157
+			throw new ApplicationLogicException("Password reset failed. Please try again.");
158
+		}
159
+
160
+		$statement = $database->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
161
+		$statement->execute([':user' => $user->getId()]);
162
+
163
+		/** @var Credential $credential */
164
+		$credential = $statement->fetchObject(Credential::class);
165
+
166
+		$statement->closeCursor();
167
+
168
+		if ($credential === false) {
169
+			throw new ApplicationLogicException("Password reset failed. Please try again.");
170
+		}
171
+
172
+		$credential->setDatabase($database);
173
+
174
+		$encryptionHelper = new EncryptionHelper($this->getSiteConfiguration());
175
+		if ($encryptionHelper->decryptData($credential->getData()) != $si) {
176
+			throw new ApplicationLogicException("Password reset failed. Please try again.");
177
+		}
178
+
179
+		if ($credential->getTimeout() < new DateTimeImmutable()) {
180
+			$credential->delete();
181
+			throw new ApplicationLogicException("Password reset token expired. Please try again.");
182
+		}
183
+
184
+		return $user;
185
+	}
186
+
187
+	/**
188
+	 * Performs the setting of the new password
189
+	 *
190
+	 * @param User $user The user to set the password for
191
+	 *
192
+	 * @throws ApplicationLogicException
193
+	 */
194
+	private function doReset(User $user)
195
+	{
196
+		$pw = WebRequest::postString('pw');
197
+		$pw2 = WebRequest::postString('pw2');
198
+
199
+		if ($pw !== $pw2) {
200
+			throw new ApplicationLogicException('Passwords do not match!');
201
+		}
202
+
203
+		$passwordCredentialProvider = new PasswordCredentialProvider($user->getDatabase(), $this->getSiteConfiguration());
204
+		$passwordCredentialProvider->setCredential($user, 1, $pw);
205
+
206
+		SessionAlert::success('You may now log in!');
207
+		$this->redirect('login');
208
+	}
209
+
210
+	protected function isProtectedPage()
211
+	{
212
+		return false;
213
+	}
214
+
215
+	/**
216
+	 * @param $user
217
+	 */
218
+	private function cleanExistingTokens($user): void
219
+	{
220
+		// clean out existing reset tokens
221
+		$statement = $this->getDatabase()->prepare("SELECT * FROM credential WHERE type = 'reset' AND user = :user;");
222
+		$statement->execute([':user' => $user->getId()]);
223
+		$existing = $statement->fetchAll(PdoDatabase::FETCH_CLASS, Credential::class);
224
+
225
+		foreach ($existing as $c) {
226
+			$c->setDatabase($this->getDatabase());
227
+			$c->delete();
228
+		}
229
+	}
230 230
 }
Please login to merge, or discard this patch.
includes/Pages/Registration/PageRegisterBase.php 1 patch
Indentation   +219 added lines, -219 removed lines patch added patch discarded remove patch
@@ -22,222 +22,222 @@
 block discarded – undo
22 22
 
23 23
 abstract class PageRegisterBase extends InternalPageBase
24 24
 {
25
-    /**
26
-     * Main function for this page, when no specific actions are called.
27
-     * @throws AccessDeniedException
28
-     * @throws ApplicationLogicException
29
-     * @throws Exception
30
-     */
31
-    protected function main()
32
-    {
33
-        $useOAuthSignup = $this->getSiteConfiguration()->getUseOAuthSignup();
34
-        if (! $this->getSiteConfiguration()->isRegistrationAllowed()) {
35
-           throw new AccessDeniedException();
36
-        }
37
-
38
-        // Dual-mode page
39
-        if (WebRequest::wasPosted()) {
40
-            $this->validateCSRFToken();
41
-
42
-            try {
43
-                $this->handlePost($useOAuthSignup);
44
-            }
45
-            catch (ApplicationLogicException $ex) {
46
-                SessionAlert::error($ex->getMessage());
47
-
48
-                $this->getDatabase()->rollBack();
49
-
50
-                $this->assignCSRFToken();
51
-                $this->assign("useOAuthSignup", $useOAuthSignup);
52
-                $this->applyErrorValues();
53
-                $this->setTemplate($this->getRegistrationTemplate());
54
-                $this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
55
-            }
56
-        }
57
-        else {
58
-            $this->assignCSRFToken();
59
-            $this->assign("useOAuthSignup", $useOAuthSignup);
60
-            $this->setTemplate($this->getRegistrationTemplate());
61
-            $this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
62
-        }
63
-    }
64
-
65
-    protected abstract function getRegistrationTemplate();
66
-
67
-    protected function isProtectedPage()
68
-    {
69
-        return false;
70
-    }
71
-
72
-    /**
73
-     * @param string $emailAddress
74
-     *
75
-     * @throws ApplicationLogicException
76
-     */
77
-    protected function validateUniqueEmail($emailAddress)
78
-    {
79
-        $query = 'SELECT COUNT(id) FROM user WHERE email = :email';
80
-        $statement = $this->getDatabase()->prepare($query);
81
-        $statement->execute(array(':email' => $emailAddress));
82
-
83
-        if ($statement->fetchColumn() > 0) {
84
-            throw new ApplicationLogicException('That email address is already in use on this system.');
85
-        }
86
-
87
-        $statement->closeCursor();
88
-    }
89
-
90
-    /**
91
-     * @param $emailAddress
92
-     * @param $password
93
-     * @param $username
94
-     * @param $useOAuthSignup
95
-     * @param $confirmationId
96
-     * @param $onwikiUsername
97
-     *
98
-     * @throws ApplicationLogicException
99
-     */
100
-    protected function validateRequest(
101
-        $emailAddress,
102
-        $password,
103
-        $username,
104
-        $useOAuthSignup,
105
-        $confirmationId,
106
-        $onwikiUsername
107
-    ) {
108
-        if (!WebRequest::postBoolean('guidelines')) {
109
-            throw new ApplicationLogicException('You must read the interface guidelines before your request may be submitted.');
110
-        }
111
-
112
-        $this->validateGeneralInformation($emailAddress, $password, $username);
113
-        $this->validateUniqueEmail($emailAddress);
114
-        $this->validateNonOAuthFields($useOAuthSignup, $confirmationId, $onwikiUsername);
115
-    }
116
-
117
-    /**
118
-     * @param $useOAuthSignup
119
-     * @param $confirmationId
120
-     * @param $onwikiUsername
121
-     *
122
-     * @throws ApplicationLogicException
123
-     */
124
-    protected function validateNonOAuthFields($useOAuthSignup, $confirmationId, $onwikiUsername)
125
-    {
126
-        if (!$useOAuthSignup) {
127
-            if ($confirmationId === null || $confirmationId <= 0) {
128
-                throw new ApplicationLogicException('Please enter the revision id of your confirmation edit.');
129
-            }
130
-
131
-            if ($onwikiUsername === null) {
132
-                throw new ApplicationLogicException('Please specify your on-wiki username.');
133
-            }
134
-        }
135
-    }
136
-
137
-    /**
138
-     * @param $emailAddress
139
-     * @param $password
140
-     * @param $username
141
-     *
142
-     * @throws ApplicationLogicException
143
-     */
144
-    protected function validateGeneralInformation($emailAddress, $password, $username)
145
-    {
146
-        if ($emailAddress === null) {
147
-            throw new ApplicationLogicException('Your email address appears to be invalid!');
148
-        }
149
-
150
-        if ($password !== WebRequest::postString('pass2')) {
151
-            throw new ApplicationLogicException('Your passwords did not match, please try again.');
152
-        }
153
-
154
-        if (User::getByUsername($username, $this->getDatabase()) !== false) {
155
-            throw new ApplicationLogicException('That username is already in use on this system.');
156
-        }
157
-    }
158
-
159
-    /**
160
-     * @param $useOAuthSignup
161
-     *
162
-     * @throws ApplicationLogicException
163
-     * @throws Exception
164
-     */
165
-    protected function handlePost($useOAuthSignup)
166
-    {
167
-        // Get the data
168
-        $emailAddress = WebRequest::postEmail('email');
169
-        $password = WebRequest::postString('pass');
170
-        $username = WebRequest::postString('name');
171
-
172
-        // Only set if OAuth is disabled
173
-        $confirmationId = WebRequest::postInt('conf_revid');
174
-        $onwikiUsername = WebRequest::postString('wname');
175
-
176
-        // Do some validation
177
-        $this->validateRequest($emailAddress, $password, $username, $useOAuthSignup, $confirmationId,
178
-            $onwikiUsername);
179
-
180
-        $database = $this->getDatabase();
181
-
182
-        $user = new User();
183
-        $user->setDatabase($database);
184
-
185
-        $user->setUsername($username);
186
-        $user->setEmail($emailAddress);
187
-
188
-        if (!$useOAuthSignup) {
189
-            $user->setOnWikiName($onwikiUsername);
190
-            $user->setConfirmationDiff($confirmationId);
191
-        }
192
-
193
-        $user->save();
194
-
195
-        $passwordCredentialProvider = new PasswordCredentialProvider($database, $this->getSiteConfiguration());
196
-        $passwordCredentialProvider->setCredential($user, 1, $password);
197
-
198
-        $defaultRole = $this->getDefaultRole();
199
-
200
-        $role = new UserRole();
201
-        $role->setDatabase($database);
202
-        $role->setUser($user->getId());
203
-        $role->setRole($defaultRole);
204
-        $role->save();
205
-
206
-        // Log now to get the signup date.
207
-        Logger::newUser($database, $user);
208
-        Logger::userRolesEdited($database, $user, 'Registration', array($defaultRole), array());
209
-
210
-        if ($useOAuthSignup) {
211
-            $oauthProtocolHelper = $this->getOAuthProtocolHelper();
212
-            $oauth = new OAuthUserHelper($user, $database, $oauthProtocolHelper, $this->getSiteConfiguration());
213
-
214
-            $authoriseUrl = $oauth->getRequestToken();
215
-            WebRequest::setOAuthPartialLogin($user);
216
-            $this->redirectUrl($authoriseUrl);
217
-        }
218
-        else {
219
-            // only notify if we're not using the oauth signup.
220
-            $this->getNotificationHelper()->userNew($user);
221
-            WebRequest::setLoggedInUser($user);
222
-            $this->redirect('preferences');
223
-        }
224
-    }
225
-
226
-    protected abstract function getDefaultRole();
227
-
228
-    /**
229
-     * Entry point for registration complete
230
-     * @throws Exception
231
-     */
232
-    protected function done()
233
-    {
234
-        $this->setTemplate('registration/alert-registrationcomplete.tpl');
235
-    }
236
-
237
-    protected function applyErrorValues()
238
-    {
239
-        $this->assign('tplUsername', WebRequest::postString('name'));
240
-        $this->assign('tplEmail', WebRequest::postString('email'));
241
-        $this->assign('tplWikipediaUsername', WebRequest::postString('wname'));
242
-        $this->assign('tplConfRevId', WebRequest::postInt('conf_revid'));
243
-    }}
25
+	/**
26
+	 * Main function for this page, when no specific actions are called.
27
+	 * @throws AccessDeniedException
28
+	 * @throws ApplicationLogicException
29
+	 * @throws Exception
30
+	 */
31
+	protected function main()
32
+	{
33
+		$useOAuthSignup = $this->getSiteConfiguration()->getUseOAuthSignup();
34
+		if (! $this->getSiteConfiguration()->isRegistrationAllowed()) {
35
+		   throw new AccessDeniedException();
36
+		}
37
+
38
+		// Dual-mode page
39
+		if (WebRequest::wasPosted()) {
40
+			$this->validateCSRFToken();
41
+
42
+			try {
43
+				$this->handlePost($useOAuthSignup);
44
+			}
45
+			catch (ApplicationLogicException $ex) {
46
+				SessionAlert::error($ex->getMessage());
47
+
48
+				$this->getDatabase()->rollBack();
49
+
50
+				$this->assignCSRFToken();
51
+				$this->assign("useOAuthSignup", $useOAuthSignup);
52
+				$this->applyErrorValues();
53
+				$this->setTemplate($this->getRegistrationTemplate());
54
+				$this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
55
+			}
56
+		}
57
+		else {
58
+			$this->assignCSRFToken();
59
+			$this->assign("useOAuthSignup", $useOAuthSignup);
60
+			$this->setTemplate($this->getRegistrationTemplate());
61
+			$this->addJs("/vendor/dropbox/zxcvbn/dist/zxcvbn.js");
62
+		}
63
+	}
64
+
65
+	protected abstract function getRegistrationTemplate();
66
+
67
+	protected function isProtectedPage()
68
+	{
69
+		return false;
70
+	}
71
+
72
+	/**
73
+	 * @param string $emailAddress
74
+	 *
75
+	 * @throws ApplicationLogicException
76
+	 */
77
+	protected function validateUniqueEmail($emailAddress)
78
+	{
79
+		$query = 'SELECT COUNT(id) FROM user WHERE email = :email';
80
+		$statement = $this->getDatabase()->prepare($query);
81
+		$statement->execute(array(':email' => $emailAddress));
82
+
83
+		if ($statement->fetchColumn() > 0) {
84
+			throw new ApplicationLogicException('That email address is already in use on this system.');
85
+		}
86
+
87
+		$statement->closeCursor();
88
+	}
89
+
90
+	/**
91
+	 * @param $emailAddress
92
+	 * @param $password
93
+	 * @param $username
94
+	 * @param $useOAuthSignup
95
+	 * @param $confirmationId
96
+	 * @param $onwikiUsername
97
+	 *
98
+	 * @throws ApplicationLogicException
99
+	 */
100
+	protected function validateRequest(
101
+		$emailAddress,
102
+		$password,
103
+		$username,
104
+		$useOAuthSignup,
105
+		$confirmationId,
106
+		$onwikiUsername
107
+	) {
108
+		if (!WebRequest::postBoolean('guidelines')) {
109
+			throw new ApplicationLogicException('You must read the interface guidelines before your request may be submitted.');
110
+		}
111
+
112
+		$this->validateGeneralInformation($emailAddress, $password, $username);
113
+		$this->validateUniqueEmail($emailAddress);
114
+		$this->validateNonOAuthFields($useOAuthSignup, $confirmationId, $onwikiUsername);
115
+	}
116
+
117
+	/**
118
+	 * @param $useOAuthSignup
119
+	 * @param $confirmationId
120
+	 * @param $onwikiUsername
121
+	 *
122
+	 * @throws ApplicationLogicException
123
+	 */
124
+	protected function validateNonOAuthFields($useOAuthSignup, $confirmationId, $onwikiUsername)
125
+	{
126
+		if (!$useOAuthSignup) {
127
+			if ($confirmationId === null || $confirmationId <= 0) {
128
+				throw new ApplicationLogicException('Please enter the revision id of your confirmation edit.');
129
+			}
130
+
131
+			if ($onwikiUsername === null) {
132
+				throw new ApplicationLogicException('Please specify your on-wiki username.');
133
+			}
134
+		}
135
+	}
136
+
137
+	/**
138
+	 * @param $emailAddress
139
+	 * @param $password
140
+	 * @param $username
141
+	 *
142
+	 * @throws ApplicationLogicException
143
+	 */
144
+	protected function validateGeneralInformation($emailAddress, $password, $username)
145
+	{
146
+		if ($emailAddress === null) {
147
+			throw new ApplicationLogicException('Your email address appears to be invalid!');
148
+		}
149
+
150
+		if ($password !== WebRequest::postString('pass2')) {
151
+			throw new ApplicationLogicException('Your passwords did not match, please try again.');
152
+		}
153
+
154
+		if (User::getByUsername($username, $this->getDatabase()) !== false) {
155
+			throw new ApplicationLogicException('That username is already in use on this system.');
156
+		}
157
+	}
158
+
159
+	/**
160
+	 * @param $useOAuthSignup
161
+	 *
162
+	 * @throws ApplicationLogicException
163
+	 * @throws Exception
164
+	 */
165
+	protected function handlePost($useOAuthSignup)
166
+	{
167
+		// Get the data
168
+		$emailAddress = WebRequest::postEmail('email');
169
+		$password = WebRequest::postString('pass');
170
+		$username = WebRequest::postString('name');
171
+
172
+		// Only set if OAuth is disabled
173
+		$confirmationId = WebRequest::postInt('conf_revid');
174
+		$onwikiUsername = WebRequest::postString('wname');
175
+
176
+		// Do some validation
177
+		$this->validateRequest($emailAddress, $password, $username, $useOAuthSignup, $confirmationId,
178
+			$onwikiUsername);
179
+
180
+		$database = $this->getDatabase();
181
+
182
+		$user = new User();
183
+		$user->setDatabase($database);
184
+
185
+		$user->setUsername($username);
186
+		$user->setEmail($emailAddress);
187
+
188
+		if (!$useOAuthSignup) {
189
+			$user->setOnWikiName($onwikiUsername);
190
+			$user->setConfirmationDiff($confirmationId);
191
+		}
192
+
193
+		$user->save();
194
+
195
+		$passwordCredentialProvider = new PasswordCredentialProvider($database, $this->getSiteConfiguration());
196
+		$passwordCredentialProvider->setCredential($user, 1, $password);
197
+
198
+		$defaultRole = $this->getDefaultRole();
199
+
200
+		$role = new UserRole();
201
+		$role->setDatabase($database);
202
+		$role->setUser($user->getId());
203
+		$role->setRole($defaultRole);
204
+		$role->save();
205
+
206
+		// Log now to get the signup date.
207
+		Logger::newUser($database, $user);
208
+		Logger::userRolesEdited($database, $user, 'Registration', array($defaultRole), array());
209
+
210
+		if ($useOAuthSignup) {
211
+			$oauthProtocolHelper = $this->getOAuthProtocolHelper();
212
+			$oauth = new OAuthUserHelper($user, $database, $oauthProtocolHelper, $this->getSiteConfiguration());
213
+
214
+			$authoriseUrl = $oauth->getRequestToken();
215
+			WebRequest::setOAuthPartialLogin($user);
216
+			$this->redirectUrl($authoriseUrl);
217
+		}
218
+		else {
219
+			// only notify if we're not using the oauth signup.
220
+			$this->getNotificationHelper()->userNew($user);
221
+			WebRequest::setLoggedInUser($user);
222
+			$this->redirect('preferences');
223
+		}
224
+	}
225
+
226
+	protected abstract function getDefaultRole();
227
+
228
+	/**
229
+	 * Entry point for registration complete
230
+	 * @throws Exception
231
+	 */
232
+	protected function done()
233
+	{
234
+		$this->setTemplate('registration/alert-registrationcomplete.tpl');
235
+	}
236
+
237
+	protected function applyErrorValues()
238
+	{
239
+		$this->assign('tplUsername', WebRequest::postString('name'));
240
+		$this->assign('tplEmail', WebRequest::postString('email'));
241
+		$this->assign('tplWikipediaUsername', WebRequest::postString('wname'));
242
+		$this->assign('tplConfRevId', WebRequest::postInt('conf_revid'));
243
+	}}
Please login to merge, or discard this patch.