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