Completed
Push — master ( b7c46f...39c63d )
by
unknown
58:06 queued 28:33
created
core/Controller/LostController.php 1 patch
Indentation   +257 added lines, -257 removed lines patch added patch discarded remove patch
@@ -56,261 +56,261 @@
 block discarded – undo
56 56
  */
57 57
 #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
58 58
 class LostController extends Controller {
59
-	public function __construct(
60
-		string $appName,
61
-		IRequest $request,
62
-		private IURLGenerator $urlGenerator,
63
-		private IUserManager $userManager,
64
-		private Defaults $defaults,
65
-		private IL10N $l10n,
66
-		private IConfig $config,
67
-		protected string $defaultMailAddress,
68
-		private IManager $encryptionManager,
69
-		private IMailer $mailer,
70
-		private LoggerInterface $logger,
71
-		private Manager $twoFactorManager,
72
-		private IInitialState $initialState,
73
-		private IVerificationToken $verificationToken,
74
-		private IEventDispatcher $eventDispatcher,
75
-		private Limiter $limiter,
76
-	) {
77
-		parent::__construct($appName, $request);
78
-	}
79
-
80
-	/**
81
-	 * Someone wants to reset their password:
82
-	 */
83
-	#[PublicPage]
84
-	#[NoCSRFRequired]
85
-	#[BruteForceProtection(action: 'passwordResetEmail')]
86
-	#[AnonRateLimit(limit: 10, period: 300)]
87
-	#[FrontpageRoute(verb: 'GET', url: '/lostpassword/reset/form/{token}/{userId}')]
88
-	public function resetform(string $token, string $userId): TemplateResponse {
89
-		try {
90
-			$this->checkPasswordResetToken($token, $userId);
91
-		} catch (Exception $e) {
92
-			if ($this->config->getSystemValue('lost_password_link', '') !== 'disabled'
93
-				|| ($e instanceof InvalidTokenException
94
-					&& !in_array($e->getCode(), [InvalidTokenException::TOKEN_NOT_FOUND, InvalidTokenException::USER_UNKNOWN]))
95
-			) {
96
-				$response = new TemplateResponse(
97
-					'core', 'error', [
98
-						'errors' => [['error' => $e->getMessage()]]
99
-					],
100
-					TemplateResponse::RENDER_AS_GUEST
101
-				);
102
-				$response->throttle();
103
-				return $response;
104
-			}
105
-			return new TemplateResponse('core', 'error', [
106
-				'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
107
-			],
108
-				TemplateResponse::RENDER_AS_GUEST
109
-			);
110
-		}
111
-		$this->initialState->provideInitialState('resetPasswordUser', $userId);
112
-		$this->initialState->provideInitialState('resetPasswordTarget',
113
-			$this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', ['userId' => $userId, 'token' => $token])
114
-		);
115
-
116
-		return new TemplateResponse(
117
-			'core',
118
-			'login',
119
-			[],
120
-			'guest'
121
-		);
122
-	}
123
-
124
-	/**
125
-	 * @throws Exception
126
-	 */
127
-	protected function checkPasswordResetToken(string $token, string $userId): void {
128
-		try {
129
-			$user = $this->userManager->get($userId);
130
-			$this->verificationToken->check($token, $user, 'lostpassword', $user ? $user->getEMailAddress() : '', true);
131
-		} catch (InvalidTokenException $e) {
132
-			$error = $e->getCode() === InvalidTokenException::TOKEN_EXPIRED
133
-				? $this->l10n->t('Could not reset password because the token is expired')
134
-				: $this->l10n->t('Could not reset password because the token is invalid');
135
-			throw new Exception($error, (int)$e->getCode(), $e);
136
-		}
137
-	}
138
-
139
-	private function error(string $message, array $additional = []): array {
140
-		return array_merge(['status' => 'error', 'msg' => $message], $additional);
141
-	}
142
-
143
-	private function success(array $data = []): array {
144
-		return array_merge($data, ['status' => 'success']);
145
-	}
146
-
147
-	#[PublicPage]
148
-	#[BruteForceProtection(action: 'passwordResetEmail')]
149
-	#[AnonRateLimit(limit: 10, period: 300)]
150
-	#[FrontpageRoute(verb: 'POST', url: '/lostpassword/email')]
151
-	public function email(string $user): JSONResponse {
152
-		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
153
-			return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
154
-		}
155
-
156
-		$user = trim($user);
157
-
158
-		if (strlen($user) > 255) {
159
-			return new JSONResponse($this->error($this->l10n->t('Unsupported email length (>255)')));
160
-		}
161
-
162
-		Util::emitHook(
163
-			'\OCA\Files_Sharing\API\Server2Server',
164
-			'preLoginNameUsedAsUserName',
165
-			['uid' => &$user]
166
-		);
167
-
168
-		// FIXME: use HTTP error codes
169
-		try {
170
-			$this->sendEmail($user);
171
-		} catch (ResetPasswordException $e) {
172
-			// Ignore the error since we do not want to leak this info
173
-			$this->logger->warning('Could not send password reset email: ' . $e->getMessage());
174
-		} catch (Exception $e) {
175
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
176
-		}
177
-
178
-		$response = new JSONResponse($this->success());
179
-		$response->throttle();
180
-		return $response;
181
-	}
182
-
183
-	#[PublicPage]
184
-	#[BruteForceProtection(action: 'passwordResetEmail')]
185
-	#[AnonRateLimit(limit: 10, period: 300)]
186
-	#[FrontpageRoute(verb: 'POST', url: '/lostpassword/set/{token}/{userId}')]
187
-	public function setPassword(string $token, string $userId, string $password, bool $proceed): JSONResponse {
188
-		if ($this->encryptionManager->isEnabled() && !$proceed) {
189
-			$encryptionModules = $this->encryptionManager->getEncryptionModules();
190
-			foreach ($encryptionModules as $module) {
191
-				/** @var IEncryptionModule $instance */
192
-				$instance = call_user_func($module['callback']);
193
-				// this way we can find out whether per-user keys are used or a system wide encryption key
194
-				if ($instance->needDetailedAccessList()) {
195
-					return new JSONResponse($this->error('', ['encryption' => true]));
196
-				}
197
-			}
198
-		}
199
-
200
-		try {
201
-			$this->checkPasswordResetToken($token, $userId);
202
-			$user = $this->userManager->get($userId);
203
-
204
-			$this->eventDispatcher->dispatchTyped(new BeforePasswordResetEvent($user, $password));
205
-			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', ['uid' => $userId, 'password' => $password]);
206
-
207
-			if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
208
-				throw new HintException('Password too long', $this->l10n->t('Password is too long. Maximum allowed length is 469 characters.'));
209
-			}
210
-
211
-			if (!$user->setPassword($password)) {
212
-				throw new Exception();
213
-			}
214
-
215
-			$this->eventDispatcher->dispatchTyped(new PasswordResetEvent($user, $password));
216
-			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]);
217
-
218
-			$this->twoFactorManager->clearTwoFactorPending($userId);
219
-
220
-			$this->config->deleteUserValue($userId, 'core', 'lostpassword');
221
-			@Server::get(Session::class)->unsetMagicInCookie();
222
-		} catch (HintException $e) {
223
-			$response = new JSONResponse($this->error($e->getHint()));
224
-			$response->throttle();
225
-			return $response;
226
-		} catch (Exception $e) {
227
-			$response = new JSONResponse($this->error($e->getMessage()));
228
-			$response->throttle();
229
-			return $response;
230
-		}
231
-
232
-		return new JSONResponse($this->success(['user' => $userId]));
233
-	}
234
-
235
-	/**
236
-	 * @throws ResetPasswordException
237
-	 * @throws PreConditionNotMetException
238
-	 */
239
-	protected function sendEmail(string $input): void {
240
-		$user = $this->findUserByIdOrMail($input);
241
-		$email = $user->getEMailAddress();
242
-
243
-		if (empty($email)) {
244
-			throw new ResetPasswordException('Could not send reset e-mail since there is no email for username ' . $input);
245
-		}
246
-
247
-		try {
248
-			$this->limiter->registerUserRequest('lostpasswordemail', 5, 1800, $user);
249
-		} catch (RateLimitExceededException $e) {
250
-			throw new ResetPasswordException('Could not send reset e-mail, 5 of them were already sent in the last 30 minutes', 0, $e);
251
-		}
252
-
253
-		// Generate the token. It is stored encrypted in the database with the
254
-		// secret being the users' email address appended with the system secret.
255
-		// This makes the token automatically invalidate once the user changes
256
-		// their email address.
257
-		$token = $this->verificationToken->create($user, 'lostpassword', $email);
258
-
259
-		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user->getUID(), 'token' => $token]);
260
-
261
-		$emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
262
-			'link' => $link,
263
-		]);
264
-
265
-		$emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
266
-		$emailTemplate->addHeader();
267
-		$emailTemplate->addHeading($this->l10n->t('Password reset'));
268
-
269
-		$emailTemplate->addBodyText(
270
-			htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
271
-			$this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
272
-		);
273
-
274
-		$emailTemplate->addBodyButton(
275
-			htmlspecialchars($this->l10n->t('Reset your password')),
276
-			$link,
277
-			false
278
-		);
279
-		$emailTemplate->addFooter();
280
-
281
-		try {
282
-			$message = $this->mailer->createMessage();
283
-			$message->setTo([$email => $user->getDisplayName()]);
284
-			$message->setFrom([$this->defaultMailAddress => $this->defaults->getName()]);
285
-			$message->useTemplate($emailTemplate);
286
-			$this->mailer->send($message);
287
-		} catch (Exception $e) {
288
-			// Log the exception and continue
289
-			$this->logger->error($e->getMessage(), ['app' => 'core', 'exception' => $e]);
290
-		}
291
-	}
292
-
293
-	/**
294
-	 * @throws ResetPasswordException
295
-	 */
296
-	protected function findUserByIdOrMail(string $input): IUser {
297
-		$user = $this->userManager->get($input);
298
-		if ($user instanceof IUser) {
299
-			if (!$user->isEnabled()) {
300
-				throw new ResetPasswordException('Account ' . $user->getUID() . ' is disabled');
301
-			}
302
-
303
-			return $user;
304
-		}
305
-
306
-		$users = array_filter($this->userManager->getByEmail($input), function (IUser $user) {
307
-			return $user->isEnabled();
308
-		});
309
-
310
-		if (count($users) === 1) {
311
-			return reset($users);
312
-		}
313
-
314
-		throw new ResetPasswordException('Could not find user ' . $input);
315
-	}
59
+    public function __construct(
60
+        string $appName,
61
+        IRequest $request,
62
+        private IURLGenerator $urlGenerator,
63
+        private IUserManager $userManager,
64
+        private Defaults $defaults,
65
+        private IL10N $l10n,
66
+        private IConfig $config,
67
+        protected string $defaultMailAddress,
68
+        private IManager $encryptionManager,
69
+        private IMailer $mailer,
70
+        private LoggerInterface $logger,
71
+        private Manager $twoFactorManager,
72
+        private IInitialState $initialState,
73
+        private IVerificationToken $verificationToken,
74
+        private IEventDispatcher $eventDispatcher,
75
+        private Limiter $limiter,
76
+    ) {
77
+        parent::__construct($appName, $request);
78
+    }
79
+
80
+    /**
81
+     * Someone wants to reset their password:
82
+     */
83
+    #[PublicPage]
84
+    #[NoCSRFRequired]
85
+    #[BruteForceProtection(action: 'passwordResetEmail')]
86
+    #[AnonRateLimit(limit: 10, period: 300)]
87
+    #[FrontpageRoute(verb: 'GET', url: '/lostpassword/reset/form/{token}/{userId}')]
88
+    public function resetform(string $token, string $userId): TemplateResponse {
89
+        try {
90
+            $this->checkPasswordResetToken($token, $userId);
91
+        } catch (Exception $e) {
92
+            if ($this->config->getSystemValue('lost_password_link', '') !== 'disabled'
93
+                || ($e instanceof InvalidTokenException
94
+                    && !in_array($e->getCode(), [InvalidTokenException::TOKEN_NOT_FOUND, InvalidTokenException::USER_UNKNOWN]))
95
+            ) {
96
+                $response = new TemplateResponse(
97
+                    'core', 'error', [
98
+                        'errors' => [['error' => $e->getMessage()]]
99
+                    ],
100
+                    TemplateResponse::RENDER_AS_GUEST
101
+                );
102
+                $response->throttle();
103
+                return $response;
104
+            }
105
+            return new TemplateResponse('core', 'error', [
106
+                'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
107
+            ],
108
+                TemplateResponse::RENDER_AS_GUEST
109
+            );
110
+        }
111
+        $this->initialState->provideInitialState('resetPasswordUser', $userId);
112
+        $this->initialState->provideInitialState('resetPasswordTarget',
113
+            $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', ['userId' => $userId, 'token' => $token])
114
+        );
115
+
116
+        return new TemplateResponse(
117
+            'core',
118
+            'login',
119
+            [],
120
+            'guest'
121
+        );
122
+    }
123
+
124
+    /**
125
+     * @throws Exception
126
+     */
127
+    protected function checkPasswordResetToken(string $token, string $userId): void {
128
+        try {
129
+            $user = $this->userManager->get($userId);
130
+            $this->verificationToken->check($token, $user, 'lostpassword', $user ? $user->getEMailAddress() : '', true);
131
+        } catch (InvalidTokenException $e) {
132
+            $error = $e->getCode() === InvalidTokenException::TOKEN_EXPIRED
133
+                ? $this->l10n->t('Could not reset password because the token is expired')
134
+                : $this->l10n->t('Could not reset password because the token is invalid');
135
+            throw new Exception($error, (int)$e->getCode(), $e);
136
+        }
137
+    }
138
+
139
+    private function error(string $message, array $additional = []): array {
140
+        return array_merge(['status' => 'error', 'msg' => $message], $additional);
141
+    }
142
+
143
+    private function success(array $data = []): array {
144
+        return array_merge($data, ['status' => 'success']);
145
+    }
146
+
147
+    #[PublicPage]
148
+    #[BruteForceProtection(action: 'passwordResetEmail')]
149
+    #[AnonRateLimit(limit: 10, period: 300)]
150
+    #[FrontpageRoute(verb: 'POST', url: '/lostpassword/email')]
151
+    public function email(string $user): JSONResponse {
152
+        if ($this->config->getSystemValue('lost_password_link', '') !== '') {
153
+            return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
154
+        }
155
+
156
+        $user = trim($user);
157
+
158
+        if (strlen($user) > 255) {
159
+            return new JSONResponse($this->error($this->l10n->t('Unsupported email length (>255)')));
160
+        }
161
+
162
+        Util::emitHook(
163
+            '\OCA\Files_Sharing\API\Server2Server',
164
+            'preLoginNameUsedAsUserName',
165
+            ['uid' => &$user]
166
+        );
167
+
168
+        // FIXME: use HTTP error codes
169
+        try {
170
+            $this->sendEmail($user);
171
+        } catch (ResetPasswordException $e) {
172
+            // Ignore the error since we do not want to leak this info
173
+            $this->logger->warning('Could not send password reset email: ' . $e->getMessage());
174
+        } catch (Exception $e) {
175
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
176
+        }
177
+
178
+        $response = new JSONResponse($this->success());
179
+        $response->throttle();
180
+        return $response;
181
+    }
182
+
183
+    #[PublicPage]
184
+    #[BruteForceProtection(action: 'passwordResetEmail')]
185
+    #[AnonRateLimit(limit: 10, period: 300)]
186
+    #[FrontpageRoute(verb: 'POST', url: '/lostpassword/set/{token}/{userId}')]
187
+    public function setPassword(string $token, string $userId, string $password, bool $proceed): JSONResponse {
188
+        if ($this->encryptionManager->isEnabled() && !$proceed) {
189
+            $encryptionModules = $this->encryptionManager->getEncryptionModules();
190
+            foreach ($encryptionModules as $module) {
191
+                /** @var IEncryptionModule $instance */
192
+                $instance = call_user_func($module['callback']);
193
+                // this way we can find out whether per-user keys are used or a system wide encryption key
194
+                if ($instance->needDetailedAccessList()) {
195
+                    return new JSONResponse($this->error('', ['encryption' => true]));
196
+                }
197
+            }
198
+        }
199
+
200
+        try {
201
+            $this->checkPasswordResetToken($token, $userId);
202
+            $user = $this->userManager->get($userId);
203
+
204
+            $this->eventDispatcher->dispatchTyped(new BeforePasswordResetEvent($user, $password));
205
+            \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', ['uid' => $userId, 'password' => $password]);
206
+
207
+            if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
208
+                throw new HintException('Password too long', $this->l10n->t('Password is too long. Maximum allowed length is 469 characters.'));
209
+            }
210
+
211
+            if (!$user->setPassword($password)) {
212
+                throw new Exception();
213
+            }
214
+
215
+            $this->eventDispatcher->dispatchTyped(new PasswordResetEvent($user, $password));
216
+            \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]);
217
+
218
+            $this->twoFactorManager->clearTwoFactorPending($userId);
219
+
220
+            $this->config->deleteUserValue($userId, 'core', 'lostpassword');
221
+            @Server::get(Session::class)->unsetMagicInCookie();
222
+        } catch (HintException $e) {
223
+            $response = new JSONResponse($this->error($e->getHint()));
224
+            $response->throttle();
225
+            return $response;
226
+        } catch (Exception $e) {
227
+            $response = new JSONResponse($this->error($e->getMessage()));
228
+            $response->throttle();
229
+            return $response;
230
+        }
231
+
232
+        return new JSONResponse($this->success(['user' => $userId]));
233
+    }
234
+
235
+    /**
236
+     * @throws ResetPasswordException
237
+     * @throws PreConditionNotMetException
238
+     */
239
+    protected function sendEmail(string $input): void {
240
+        $user = $this->findUserByIdOrMail($input);
241
+        $email = $user->getEMailAddress();
242
+
243
+        if (empty($email)) {
244
+            throw new ResetPasswordException('Could not send reset e-mail since there is no email for username ' . $input);
245
+        }
246
+
247
+        try {
248
+            $this->limiter->registerUserRequest('lostpasswordemail', 5, 1800, $user);
249
+        } catch (RateLimitExceededException $e) {
250
+            throw new ResetPasswordException('Could not send reset e-mail, 5 of them were already sent in the last 30 minutes', 0, $e);
251
+        }
252
+
253
+        // Generate the token. It is stored encrypted in the database with the
254
+        // secret being the users' email address appended with the system secret.
255
+        // This makes the token automatically invalidate once the user changes
256
+        // their email address.
257
+        $token = $this->verificationToken->create($user, 'lostpassword', $email);
258
+
259
+        $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user->getUID(), 'token' => $token]);
260
+
261
+        $emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
262
+            'link' => $link,
263
+        ]);
264
+
265
+        $emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
266
+        $emailTemplate->addHeader();
267
+        $emailTemplate->addHeading($this->l10n->t('Password reset'));
268
+
269
+        $emailTemplate->addBodyText(
270
+            htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
271
+            $this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
272
+        );
273
+
274
+        $emailTemplate->addBodyButton(
275
+            htmlspecialchars($this->l10n->t('Reset your password')),
276
+            $link,
277
+            false
278
+        );
279
+        $emailTemplate->addFooter();
280
+
281
+        try {
282
+            $message = $this->mailer->createMessage();
283
+            $message->setTo([$email => $user->getDisplayName()]);
284
+            $message->setFrom([$this->defaultMailAddress => $this->defaults->getName()]);
285
+            $message->useTemplate($emailTemplate);
286
+            $this->mailer->send($message);
287
+        } catch (Exception $e) {
288
+            // Log the exception and continue
289
+            $this->logger->error($e->getMessage(), ['app' => 'core', 'exception' => $e]);
290
+        }
291
+    }
292
+
293
+    /**
294
+     * @throws ResetPasswordException
295
+     */
296
+    protected function findUserByIdOrMail(string $input): IUser {
297
+        $user = $this->userManager->get($input);
298
+        if ($user instanceof IUser) {
299
+            if (!$user->isEnabled()) {
300
+                throw new ResetPasswordException('Account ' . $user->getUID() . ' is disabled');
301
+            }
302
+
303
+            return $user;
304
+        }
305
+
306
+        $users = array_filter($this->userManager->getByEmail($input), function (IUser $user) {
307
+            return $user->isEnabled();
308
+        });
309
+
310
+        if (count($users) === 1) {
311
+            return reset($users);
312
+        }
313
+
314
+        throw new ResetPasswordException('Could not find user ' . $input);
315
+    }
316 316
 }
Please login to merge, or discard this patch.