Passed
Push — master ( 050b91...deb7d2 )
by Roeland
10:46 queued 11s
created
lib/private/Authentication/TwoFactorAuth/Manager.php 1 patch
Indentation   +322 added lines, -322 removed lines patch added patch discarded remove patch
@@ -47,330 +47,330 @@
 block discarded – undo
47 47
 
48 48
 class Manager {
49 49
 
50
-	const SESSION_UID_KEY = 'two_factor_auth_uid';
51
-	const SESSION_UID_DONE = 'two_factor_auth_passed';
52
-	const REMEMBER_LOGIN = 'two_factor_remember_login';
53
-	const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
54
-
55
-	/** @var ProviderLoader */
56
-	private $providerLoader;
57
-
58
-	/** @var IRegistry */
59
-	private $providerRegistry;
60
-
61
-	/** @var MandatoryTwoFactor */
62
-	private $mandatoryTwoFactor;
63
-
64
-	/** @var ISession */
65
-	private $session;
66
-
67
-	/** @var IConfig */
68
-	private $config;
69
-
70
-	/** @var IManager */
71
-	private $activityManager;
72
-
73
-	/** @var ILogger */
74
-	private $logger;
75
-
76
-	/** @var TokenProvider */
77
-	private $tokenProvider;
78
-
79
-	/** @var ITimeFactory */
80
-	private $timeFactory;
81
-
82
-	/** @var EventDispatcherInterface */
83
-	private $dispatcher;
84
-
85
-	public function __construct(ProviderLoader $providerLoader,
86
-								IRegistry $providerRegistry,
87
-								MandatoryTwoFactor $mandatoryTwoFactor,
88
-								ISession $session, IConfig $config,
89
-								IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
90
-								ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
91
-		$this->providerLoader = $providerLoader;
92
-		$this->providerRegistry = $providerRegistry;
93
-		$this->mandatoryTwoFactor = $mandatoryTwoFactor;
94
-		$this->session = $session;
95
-		$this->config = $config;
96
-		$this->activityManager = $activityManager;
97
-		$this->logger = $logger;
98
-		$this->tokenProvider = $tokenProvider;
99
-		$this->timeFactory = $timeFactory;
100
-		$this->dispatcher = $eventDispatcher;
101
-	}
102
-
103
-	/**
104
-	 * Determine whether the user must provide a second factor challenge
105
-	 *
106
-	 * @param IUser $user
107
-	 * @return boolean
108
-	 */
109
-	public function isTwoFactorAuthenticated(IUser $user): bool {
110
-		if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
111
-			return true;
112
-		}
113
-
114
-		$providerStates = $this->providerRegistry->getProviderStates($user);
115
-		$providers = $this->providerLoader->getProviders($user);
116
-		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
117
-		$enabled = array_filter($fixedStates);
118
-		$providerIds = array_keys($enabled);
119
-		$providerIdsWithoutBackupCodes = array_diff($providerIds, [self::BACKUP_CODES_PROVIDER_ID]);
120
-
121
-		return !empty($providerIdsWithoutBackupCodes);
122
-	}
123
-
124
-	/**
125
-	 * Get a 2FA provider by its ID
126
-	 *
127
-	 * @param IUser $user
128
-	 * @param string $challengeProviderId
129
-	 * @return IProvider|null
130
-	 */
131
-	public function getProvider(IUser $user, string $challengeProviderId) {
132
-		$providers = $this->getProviderSet($user)->getProviders();
133
-		return $providers[$challengeProviderId] ?? null;
134
-	}
135
-
136
-	/**
137
-	 * Check if the persistant mapping of enabled/disabled state of each available
138
-	 * provider is missing an entry and add it to the registry in that case.
139
-	 *
140
-	 * @todo remove in Nextcloud 17 as by then all providers should have been updated
141
-	 *
142
-	 * @param string[] $providerStates
143
-	 * @param IProvider[] $providers
144
-	 * @param IUser $user
145
-	 * @return string[] the updated $providerStates variable
146
-	 */
147
-	private function fixMissingProviderStates(array $providerStates,
148
-		array $providers, IUser $user): array {
149
-
150
-		foreach ($providers as $provider) {
151
-			if (isset($providerStates[$provider->getId()])) {
152
-				// All good
153
-				continue;
154
-			}
155
-
156
-			$enabled = $provider->isTwoFactorAuthEnabledForUser($user);
157
-			if ($enabled) {
158
-				$this->providerRegistry->enableProviderFor($provider, $user);
159
-			} else {
160
-				$this->providerRegistry->disableProviderFor($provider, $user);
161
-			}
162
-			$providerStates[$provider->getId()] = $enabled;
163
-		}
164
-
165
-		return $providerStates;
166
-	}
167
-
168
-	/**
169
-	 * @param array $states
170
-	 * @param IProvider $providers
171
-	 */
172
-	private function isProviderMissing(array $states, array $providers): bool {
173
-		$indexed = [];
174
-		foreach ($providers as $provider) {
175
-			$indexed[$provider->getId()] = $provider;
176
-		}
177
-
178
-		$missing = [];
179
-		foreach ($states as $providerId => $enabled) {
180
-			if (!$enabled) {
181
-				// Don't care
182
-				continue;
183
-			}
184
-
185
-			if (!isset($indexed[$providerId])) {
186
-				$missing[] = $providerId;
187
-				$this->logger->alert("two-factor auth provider '$providerId' failed to load",
188
-					[
189
-					'app' => 'core',
190
-				]);
191
-			}
192
-		}
193
-
194
-		if (!empty($missing)) {
195
-			// There was at least one provider missing
196
-			$this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
197
-
198
-			return true;
199
-		}
200
-
201
-		// If we reach this, there was not a single provider missing
202
-		return false;
203
-	}
204
-
205
-	/**
206
-	 * Get the list of 2FA providers for the given user
207
-	 *
208
-	 * @param IUser $user
209
-	 * @throws Exception
210
-	 */
211
-	public function getProviderSet(IUser $user): ProviderSet {
212
-		$providerStates = $this->providerRegistry->getProviderStates($user);
213
-		$providers = $this->providerLoader->getProviders($user);
214
-
215
-		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
216
-		$isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
217
-
218
-		$enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
219
-			return $fixedStates[$provider->getId()];
220
-		});
221
-		return new ProviderSet($enabled, $isProviderMissing);
222
-	}
223
-
224
-	/**
225
-	 * Verify the given challenge
226
-	 *
227
-	 * @param string $providerId
228
-	 * @param IUser $user
229
-	 * @param string $challenge
230
-	 * @return boolean
231
-	 */
232
-	public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
233
-		$provider = $this->getProvider($user, $providerId);
234
-		if ($provider === null) {
235
-			return false;
236
-		}
237
-
238
-		$passed = $provider->verifyChallenge($user, $challenge);
239
-		if ($passed) {
240
-			if ($this->session->get(self::REMEMBER_LOGIN) === true) {
241
-				// TODO: resolve cyclic dependency and use DI
242
-				\OC::$server->getUserSession()->createRememberMeToken($user);
243
-			}
244
-			$this->session->remove(self::SESSION_UID_KEY);
245
-			$this->session->remove(self::REMEMBER_LOGIN);
246
-			$this->session->set(self::SESSION_UID_DONE, $user->getUID());
247
-
248
-			// Clear token from db
249
-			$sessionId = $this->session->getId();
250
-			$token = $this->tokenProvider->getToken($sessionId);
251
-			$tokenId = $token->getId();
252
-			$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
253
-
254
-			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
255
-			$this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
256
-
257
-			$this->publishEvent($user, 'twofactor_success', [
258
-				'provider' => $provider->getDisplayName(),
259
-			]);
260
-		} else {
261
-			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
262
-			$this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
263
-
264
-			$this->publishEvent($user, 'twofactor_failed', [
265
-				'provider' => $provider->getDisplayName(),
266
-			]);
267
-		}
268
-		return $passed;
269
-	}
270
-
271
-	/**
272
-	 * Push a 2fa event the user's activity stream
273
-	 *
274
-	 * @param IUser $user
275
-	 * @param string $event
276
-	 * @param array $params
277
-	 */
278
-	private function publishEvent(IUser $user, string $event, array $params) {
279
-		$activity = $this->activityManager->generateEvent();
280
-		$activity->setApp('core')
281
-			->setType('security')
282
-			->setAuthor($user->getUID())
283
-			->setAffectedUser($user->getUID())
284
-			->setSubject($event, $params);
285
-		try {
286
-			$this->activityManager->publish($activity);
287
-		} catch (BadMethodCallException $e) {
288
-			$this->logger->warning('could not publish activity', ['app' => 'core']);
289
-			$this->logger->logException($e, ['app' => 'core']);
290
-		}
291
-	}
292
-
293
-	/**
294
-	 * Check if the currently logged in user needs to pass 2FA
295
-	 *
296
-	 * @param IUser $user the currently logged in user
297
-	 * @return boolean
298
-	 */
299
-	public function needsSecondFactor(IUser $user = null): bool {
300
-		if ($user === null) {
301
-			return false;
302
-		}
303
-
304
-		// If we are authenticated using an app password skip all this
305
-		if ($this->session->exists('app_password')) {
306
-			return false;
307
-		}
308
-
309
-		// First check if the session tells us we should do 2FA (99% case)
310
-		if (!$this->session->exists(self::SESSION_UID_KEY)) {
311
-
312
-			// Check if the session tells us it is 2FA authenticated already
313
-			if ($this->session->exists(self::SESSION_UID_DONE) &&
314
-				$this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
315
-				return false;
316
-			}
317
-
318
-			/*
50
+    const SESSION_UID_KEY = 'two_factor_auth_uid';
51
+    const SESSION_UID_DONE = 'two_factor_auth_passed';
52
+    const REMEMBER_LOGIN = 'two_factor_remember_login';
53
+    const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
54
+
55
+    /** @var ProviderLoader */
56
+    private $providerLoader;
57
+
58
+    /** @var IRegistry */
59
+    private $providerRegistry;
60
+
61
+    /** @var MandatoryTwoFactor */
62
+    private $mandatoryTwoFactor;
63
+
64
+    /** @var ISession */
65
+    private $session;
66
+
67
+    /** @var IConfig */
68
+    private $config;
69
+
70
+    /** @var IManager */
71
+    private $activityManager;
72
+
73
+    /** @var ILogger */
74
+    private $logger;
75
+
76
+    /** @var TokenProvider */
77
+    private $tokenProvider;
78
+
79
+    /** @var ITimeFactory */
80
+    private $timeFactory;
81
+
82
+    /** @var EventDispatcherInterface */
83
+    private $dispatcher;
84
+
85
+    public function __construct(ProviderLoader $providerLoader,
86
+                                IRegistry $providerRegistry,
87
+                                MandatoryTwoFactor $mandatoryTwoFactor,
88
+                                ISession $session, IConfig $config,
89
+                                IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
90
+                                ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
91
+        $this->providerLoader = $providerLoader;
92
+        $this->providerRegistry = $providerRegistry;
93
+        $this->mandatoryTwoFactor = $mandatoryTwoFactor;
94
+        $this->session = $session;
95
+        $this->config = $config;
96
+        $this->activityManager = $activityManager;
97
+        $this->logger = $logger;
98
+        $this->tokenProvider = $tokenProvider;
99
+        $this->timeFactory = $timeFactory;
100
+        $this->dispatcher = $eventDispatcher;
101
+    }
102
+
103
+    /**
104
+     * Determine whether the user must provide a second factor challenge
105
+     *
106
+     * @param IUser $user
107
+     * @return boolean
108
+     */
109
+    public function isTwoFactorAuthenticated(IUser $user): bool {
110
+        if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
111
+            return true;
112
+        }
113
+
114
+        $providerStates = $this->providerRegistry->getProviderStates($user);
115
+        $providers = $this->providerLoader->getProviders($user);
116
+        $fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
117
+        $enabled = array_filter($fixedStates);
118
+        $providerIds = array_keys($enabled);
119
+        $providerIdsWithoutBackupCodes = array_diff($providerIds, [self::BACKUP_CODES_PROVIDER_ID]);
120
+
121
+        return !empty($providerIdsWithoutBackupCodes);
122
+    }
123
+
124
+    /**
125
+     * Get a 2FA provider by its ID
126
+     *
127
+     * @param IUser $user
128
+     * @param string $challengeProviderId
129
+     * @return IProvider|null
130
+     */
131
+    public function getProvider(IUser $user, string $challengeProviderId) {
132
+        $providers = $this->getProviderSet($user)->getProviders();
133
+        return $providers[$challengeProviderId] ?? null;
134
+    }
135
+
136
+    /**
137
+     * Check if the persistant mapping of enabled/disabled state of each available
138
+     * provider is missing an entry and add it to the registry in that case.
139
+     *
140
+     * @todo remove in Nextcloud 17 as by then all providers should have been updated
141
+     *
142
+     * @param string[] $providerStates
143
+     * @param IProvider[] $providers
144
+     * @param IUser $user
145
+     * @return string[] the updated $providerStates variable
146
+     */
147
+    private function fixMissingProviderStates(array $providerStates,
148
+        array $providers, IUser $user): array {
149
+
150
+        foreach ($providers as $provider) {
151
+            if (isset($providerStates[$provider->getId()])) {
152
+                // All good
153
+                continue;
154
+            }
155
+
156
+            $enabled = $provider->isTwoFactorAuthEnabledForUser($user);
157
+            if ($enabled) {
158
+                $this->providerRegistry->enableProviderFor($provider, $user);
159
+            } else {
160
+                $this->providerRegistry->disableProviderFor($provider, $user);
161
+            }
162
+            $providerStates[$provider->getId()] = $enabled;
163
+        }
164
+
165
+        return $providerStates;
166
+    }
167
+
168
+    /**
169
+     * @param array $states
170
+     * @param IProvider $providers
171
+     */
172
+    private function isProviderMissing(array $states, array $providers): bool {
173
+        $indexed = [];
174
+        foreach ($providers as $provider) {
175
+            $indexed[$provider->getId()] = $provider;
176
+        }
177
+
178
+        $missing = [];
179
+        foreach ($states as $providerId => $enabled) {
180
+            if (!$enabled) {
181
+                // Don't care
182
+                continue;
183
+            }
184
+
185
+            if (!isset($indexed[$providerId])) {
186
+                $missing[] = $providerId;
187
+                $this->logger->alert("two-factor auth provider '$providerId' failed to load",
188
+                    [
189
+                    'app' => 'core',
190
+                ]);
191
+            }
192
+        }
193
+
194
+        if (!empty($missing)) {
195
+            // There was at least one provider missing
196
+            $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
197
+
198
+            return true;
199
+        }
200
+
201
+        // If we reach this, there was not a single provider missing
202
+        return false;
203
+    }
204
+
205
+    /**
206
+     * Get the list of 2FA providers for the given user
207
+     *
208
+     * @param IUser $user
209
+     * @throws Exception
210
+     */
211
+    public function getProviderSet(IUser $user): ProviderSet {
212
+        $providerStates = $this->providerRegistry->getProviderStates($user);
213
+        $providers = $this->providerLoader->getProviders($user);
214
+
215
+        $fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
216
+        $isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
217
+
218
+        $enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
219
+            return $fixedStates[$provider->getId()];
220
+        });
221
+        return new ProviderSet($enabled, $isProviderMissing);
222
+    }
223
+
224
+    /**
225
+     * Verify the given challenge
226
+     *
227
+     * @param string $providerId
228
+     * @param IUser $user
229
+     * @param string $challenge
230
+     * @return boolean
231
+     */
232
+    public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
233
+        $provider = $this->getProvider($user, $providerId);
234
+        if ($provider === null) {
235
+            return false;
236
+        }
237
+
238
+        $passed = $provider->verifyChallenge($user, $challenge);
239
+        if ($passed) {
240
+            if ($this->session->get(self::REMEMBER_LOGIN) === true) {
241
+                // TODO: resolve cyclic dependency and use DI
242
+                \OC::$server->getUserSession()->createRememberMeToken($user);
243
+            }
244
+            $this->session->remove(self::SESSION_UID_KEY);
245
+            $this->session->remove(self::REMEMBER_LOGIN);
246
+            $this->session->set(self::SESSION_UID_DONE, $user->getUID());
247
+
248
+            // Clear token from db
249
+            $sessionId = $this->session->getId();
250
+            $token = $this->tokenProvider->getToken($sessionId);
251
+            $tokenId = $token->getId();
252
+            $this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
253
+
254
+            $dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
255
+            $this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
256
+
257
+            $this->publishEvent($user, 'twofactor_success', [
258
+                'provider' => $provider->getDisplayName(),
259
+            ]);
260
+        } else {
261
+            $dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
262
+            $this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
263
+
264
+            $this->publishEvent($user, 'twofactor_failed', [
265
+                'provider' => $provider->getDisplayName(),
266
+            ]);
267
+        }
268
+        return $passed;
269
+    }
270
+
271
+    /**
272
+     * Push a 2fa event the user's activity stream
273
+     *
274
+     * @param IUser $user
275
+     * @param string $event
276
+     * @param array $params
277
+     */
278
+    private function publishEvent(IUser $user, string $event, array $params) {
279
+        $activity = $this->activityManager->generateEvent();
280
+        $activity->setApp('core')
281
+            ->setType('security')
282
+            ->setAuthor($user->getUID())
283
+            ->setAffectedUser($user->getUID())
284
+            ->setSubject($event, $params);
285
+        try {
286
+            $this->activityManager->publish($activity);
287
+        } catch (BadMethodCallException $e) {
288
+            $this->logger->warning('could not publish activity', ['app' => 'core']);
289
+            $this->logger->logException($e, ['app' => 'core']);
290
+        }
291
+    }
292
+
293
+    /**
294
+     * Check if the currently logged in user needs to pass 2FA
295
+     *
296
+     * @param IUser $user the currently logged in user
297
+     * @return boolean
298
+     */
299
+    public function needsSecondFactor(IUser $user = null): bool {
300
+        if ($user === null) {
301
+            return false;
302
+        }
303
+
304
+        // If we are authenticated using an app password skip all this
305
+        if ($this->session->exists('app_password')) {
306
+            return false;
307
+        }
308
+
309
+        // First check if the session tells us we should do 2FA (99% case)
310
+        if (!$this->session->exists(self::SESSION_UID_KEY)) {
311
+
312
+            // Check if the session tells us it is 2FA authenticated already
313
+            if ($this->session->exists(self::SESSION_UID_DONE) &&
314
+                $this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
315
+                return false;
316
+            }
317
+
318
+            /*
319 319
 			 * If the session is expired check if we are not logged in by a token
320 320
 			 * that still needs 2FA auth
321 321
 			 */
322
-			try {
323
-				$sessionId = $this->session->getId();
324
-				$token = $this->tokenProvider->getToken($sessionId);
325
-				$tokenId = $token->getId();
326
-				$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
327
-
328
-				if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
329
-					$this->session->set(self::SESSION_UID_DONE, $user->getUID());
330
-					return false;
331
-				}
332
-			} catch (InvalidTokenException $e) {
333
-			}
334
-		}
335
-
336
-		if (!$this->isTwoFactorAuthenticated($user)) {
337
-			// There is no second factor any more -> let the user pass
338
-			//   This prevents infinite redirect loops when a user is about
339
-			//   to solve the 2FA challenge, and the provider app is
340
-			//   disabled the same time
341
-			$this->session->remove(self::SESSION_UID_KEY);
342
-
343
-			$keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
344
-			foreach ($keys as $key) {
345
-				$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
346
-			}
347
-			return false;
348
-		}
349
-
350
-		return true;
351
-	}
352
-
353
-	/**
354
-	 * Prepare the 2FA login
355
-	 *
356
-	 * @param IUser $user
357
-	 * @param boolean $rememberMe
358
-	 */
359
-	public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
360
-		$this->session->set(self::SESSION_UID_KEY, $user->getUID());
361
-		$this->session->set(self::REMEMBER_LOGIN, $rememberMe);
362
-
363
-		$id = $this->session->getId();
364
-		$token = $this->tokenProvider->getToken($id);
365
-		$this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
366
-	}
367
-
368
-	public function clearTwoFactorPending(string $userId) {
369
-		$tokensNeeding2FA = $this->config->getUserKeys($userId, 'login_token_2fa');
370
-
371
-		foreach ($tokensNeeding2FA as $tokenId) {
372
-			$this->tokenProvider->invalidateTokenById($userId, $tokenId);
373
-		}
374
-	}
322
+            try {
323
+                $sessionId = $this->session->getId();
324
+                $token = $this->tokenProvider->getToken($sessionId);
325
+                $tokenId = $token->getId();
326
+                $tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
327
+
328
+                if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
329
+                    $this->session->set(self::SESSION_UID_DONE, $user->getUID());
330
+                    return false;
331
+                }
332
+            } catch (InvalidTokenException $e) {
333
+            }
334
+        }
335
+
336
+        if (!$this->isTwoFactorAuthenticated($user)) {
337
+            // There is no second factor any more -> let the user pass
338
+            //   This prevents infinite redirect loops when a user is about
339
+            //   to solve the 2FA challenge, and the provider app is
340
+            //   disabled the same time
341
+            $this->session->remove(self::SESSION_UID_KEY);
342
+
343
+            $keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
344
+            foreach ($keys as $key) {
345
+                $this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
346
+            }
347
+            return false;
348
+        }
349
+
350
+        return true;
351
+    }
352
+
353
+    /**
354
+     * Prepare the 2FA login
355
+     *
356
+     * @param IUser $user
357
+     * @param boolean $rememberMe
358
+     */
359
+    public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
360
+        $this->session->set(self::SESSION_UID_KEY, $user->getUID());
361
+        $this->session->set(self::REMEMBER_LOGIN, $rememberMe);
362
+
363
+        $id = $this->session->getId();
364
+        $token = $this->tokenProvider->getToken($id);
365
+        $this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
366
+    }
367
+
368
+    public function clearTwoFactorPending(string $userId) {
369
+        $tokensNeeding2FA = $this->config->getUserKeys($userId, 'login_token_2fa');
370
+
371
+        foreach ($tokensNeeding2FA as $tokenId) {
372
+            $this->tokenProvider->invalidateTokenById($userId, $tokenId);
373
+        }
374
+    }
375 375
 
376 376
 }
Please login to merge, or discard this patch.
core/Controller/LostController.php 1 patch
Indentation   +340 added lines, -340 removed lines patch added patch discarded remove patch
@@ -59,344 +59,344 @@
 block discarded – undo
59 59
  * @package OC\Core\Controller
60 60
  */
61 61
 class LostController extends Controller {
62
-	/** @var IURLGenerator */
63
-	protected $urlGenerator;
64
-	/** @var IUserManager */
65
-	protected $userManager;
66
-	/** @var Defaults */
67
-	protected $defaults;
68
-	/** @var IL10N */
69
-	protected $l10n;
70
-	/** @var string */
71
-	protected $from;
72
-	/** @var IManager */
73
-	protected $encryptionManager;
74
-	/** @var IConfig */
75
-	protected $config;
76
-	/** @var ISecureRandom */
77
-	protected $secureRandom;
78
-	/** @var IMailer */
79
-	protected $mailer;
80
-	/** @var ITimeFactory */
81
-	protected $timeFactory;
82
-	/** @var ICrypto */
83
-	protected $crypto;
84
-	/** @var ILogger */
85
-	private $logger;
86
-	/** @var Manager */
87
-	private $twoFactorManager;
88
-
89
-	/**
90
-	 * @param string $appName
91
-	 * @param IRequest $request
92
-	 * @param IURLGenerator $urlGenerator
93
-	 * @param IUserManager $userManager
94
-	 * @param Defaults $defaults
95
-	 * @param IL10N $l10n
96
-	 * @param IConfig $config
97
-	 * @param ISecureRandom $secureRandom
98
-	 * @param string $defaultMailAddress
99
-	 * @param IManager $encryptionManager
100
-	 * @param IMailer $mailer
101
-	 * @param ITimeFactory $timeFactory
102
-	 * @param ICrypto $crypto
103
-	 */
104
-	public function __construct($appName,
105
-								IRequest $request,
106
-								IURLGenerator $urlGenerator,
107
-								IUserManager $userManager,
108
-								Defaults $defaults,
109
-								IL10N $l10n,
110
-								IConfig $config,
111
-								ISecureRandom $secureRandom,
112
-								$defaultMailAddress,
113
-								IManager $encryptionManager,
114
-								IMailer $mailer,
115
-								ITimeFactory $timeFactory,
116
-								ICrypto $crypto,
117
-								ILogger $logger,
118
-								Manager $twoFactorManager) {
119
-		parent::__construct($appName, $request);
120
-		$this->urlGenerator = $urlGenerator;
121
-		$this->userManager = $userManager;
122
-		$this->defaults = $defaults;
123
-		$this->l10n = $l10n;
124
-		$this->secureRandom = $secureRandom;
125
-		$this->from = $defaultMailAddress;
126
-		$this->encryptionManager = $encryptionManager;
127
-		$this->config = $config;
128
-		$this->mailer = $mailer;
129
-		$this->timeFactory = $timeFactory;
130
-		$this->crypto = $crypto;
131
-		$this->logger = $logger;
132
-		$this->twoFactorManager = $twoFactorManager;
133
-	}
134
-
135
-	/**
136
-	 * Someone wants to reset their password:
137
-	 *
138
-	 * @PublicPage
139
-	 * @NoCSRFRequired
140
-	 *
141
-	 * @param string $token
142
-	 * @param string $userId
143
-	 * @return TemplateResponse
144
-	 */
145
-	public function resetform($token, $userId) {
146
-		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
147
-			return new TemplateResponse('core', 'error', [
148
-					'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
149
-				],
150
-				'guest'
151
-			);
152
-		}
153
-
154
-		try {
155
-			$this->checkPasswordResetToken($token, $userId);
156
-		} catch (\Exception $e) {
157
-			return new TemplateResponse(
158
-				'core', 'error', [
159
-					"errors" => array(array("error" => $e->getMessage()))
160
-				],
161
-				'guest'
162
-			);
163
-		}
164
-
165
-		return new TemplateResponse(
166
-			'core',
167
-			'lostpassword/resetpassword',
168
-			array(
169
-				'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
170
-			),
171
-			'guest'
172
-		);
173
-	}
174
-
175
-	/**
176
-	 * @param string $token
177
-	 * @param string $userId
178
-	 * @throws \Exception
179
-	 */
180
-	protected function checkPasswordResetToken($token, $userId) {
181
-		$user = $this->userManager->get($userId);
182
-		if($user === null || !$user->isEnabled()) {
183
-			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
184
-		}
185
-
186
-		try {
187
-			$encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
188
-			$mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
189
-			$decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
190
-		} catch (\Exception $e) {
191
-			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
192
-		}
193
-
194
-		$splittedToken = explode(':', $decryptedToken);
195
-		if(count($splittedToken) !== 2) {
196
-			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
197
-		}
198
-
199
-		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*24*7) ||
200
-			$user->getLastLogin() > $splittedToken[0]) {
201
-			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
202
-		}
203
-
204
-		if (!hash_equals($splittedToken[1], $token)) {
205
-			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * @param $message
211
-	 * @param array $additional
212
-	 * @return array
213
-	 */
214
-	private function error($message, array $additional=array()) {
215
-		return array_merge(array('status' => 'error', 'msg' => $message), $additional);
216
-	}
217
-
218
-	/**
219
-	 * @param array $data
220
-	 * @return array
221
-	 */
222
-	private function success($data = []) {
223
-		return array_merge($data, ['status'=>'success']);
224
-	}
225
-
226
-	/**
227
-	 * @PublicPage
228
-	 * @BruteForceProtection(action=passwordResetEmail)
229
-	 * @AnonRateThrottle(limit=10, period=300)
230
-	 *
231
-	 * @param string $user
232
-	 * @return JSONResponse
233
-	 */
234
-	public function email($user){
235
-		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
236
-			return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
237
-		}
238
-
239
-		\OCP\Util::emitHook(
240
-			'\OCA\Files_Sharing\API\Server2Server',
241
-			'preLoginNameUsedAsUserName',
242
-			['uid' => &$user]
243
-		);
244
-
245
-		// FIXME: use HTTP error codes
246
-		try {
247
-			$this->sendEmail($user);
248
-		} catch (\Exception $e) {
249
-			// Ignore the error since we do not want to leak this info
250
-			$this->logger->logException($e, [
251
-				'level' => ILogger::WARN
252
-			]);
253
-		}
254
-
255
-		$response = new JSONResponse($this->success());
256
-		$response->throttle();
257
-		return $response;
258
-	}
259
-
260
-	/**
261
-	 * @PublicPage
262
-	 * @param string $token
263
-	 * @param string $userId
264
-	 * @param string $password
265
-	 * @param boolean $proceed
266
-	 * @return array
267
-	 */
268
-	public function setPassword($token, $userId, $password, $proceed) {
269
-		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
270
-			return $this->error($this->l10n->t('Password reset is disabled'));
271
-		}
272
-
273
-		if ($this->encryptionManager->isEnabled() && !$proceed) {
274
-			$encryptionModules = $this->encryptionManager->getEncryptionModules();
275
-			foreach ($encryptionModules as $module) {
276
-				/** @var IEncryptionModule $instance */
277
-				$instance = call_user_func($module['callback']);
278
-				// this way we can find out whether per-user keys are used or a system wide encryption key
279
-				if ($instance->needDetailedAccessList()) {
280
-					return $this->error('', array('encryption' => true));
281
-				}
282
-			}
283
-		}
284
-
285
-		try {
286
-			$this->checkPasswordResetToken($token, $userId);
287
-			$user = $this->userManager->get($userId);
288
-
289
-			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
290
-
291
-			if (!$user->setPassword($password)) {
292
-				throw new \Exception();
293
-			}
294
-
295
-			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
296
-
297
-			$this->twoFactorManager->clearTwoFactorPending($userId);
298
-
299
-			$this->config->deleteUserValue($userId, 'core', 'lostpassword');
300
-			@\OC::$server->getUserSession()->unsetMagicInCookie();
301
-		} catch (HintException $e){
302
-			return $this->error($e->getHint());
303
-		} catch (\Exception $e){
304
-			return $this->error($e->getMessage());
305
-		}
306
-
307
-		return $this->success(['user' => $userId]);
308
-	}
309
-
310
-	/**
311
-	 * @param string $input
312
-	 * @throws \Exception
313
-	 */
314
-	protected function sendEmail($input) {
315
-		$user = $this->findUserByIdOrMail($input);
316
-		$email = $user->getEMailAddress();
317
-
318
-		if (empty($email)) {
319
-			throw new \Exception(
320
-				$this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
321
-			);
322
-		}
323
-
324
-		// Generate the token. It is stored encrypted in the database with the
325
-		// secret being the users' email address appended with the system secret.
326
-		// This makes the token automatically invalidate once the user changes
327
-		// their email address.
328
-		$token = $this->secureRandom->generate(
329
-			21,
330
-			ISecureRandom::CHAR_DIGITS.
331
-			ISecureRandom::CHAR_LOWER.
332
-			ISecureRandom::CHAR_UPPER
333
-		);
334
-		$tokenValue = $this->timeFactory->getTime() .':'. $token;
335
-		$encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
336
-		$this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
337
-
338
-		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
339
-
340
-		$emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
341
-			'link' => $link,
342
-		]);
343
-
344
-		$emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
345
-		$emailTemplate->addHeader();
346
-		$emailTemplate->addHeading($this->l10n->t('Password reset'));
347
-
348
-		$emailTemplate->addBodyText(
349
-			htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
350
-			$this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
351
-		);
352
-
353
-		$emailTemplate->addBodyButton(
354
-			htmlspecialchars($this->l10n->t('Reset your password')),
355
-			$link,
356
-			false
357
-		);
358
-		$emailTemplate->addFooter();
359
-
360
-		try {
361
-			$message = $this->mailer->createMessage();
362
-			$message->setTo([$email => $user->getUID()]);
363
-			$message->setFrom([$this->from => $this->defaults->getName()]);
364
-			$message->useTemplate($emailTemplate);
365
-			$this->mailer->send($message);
366
-		} catch (\Exception $e) {
367
-			throw new \Exception($this->l10n->t(
368
-				'Couldn\'t send reset email. Please contact your administrator.'
369
-			));
370
-		}
371
-	}
372
-
373
-	/**
374
-	 * @param string $input
375
-	 * @return IUser
376
-	 * @throws \InvalidArgumentException
377
-	 */
378
-	protected function findUserByIdOrMail($input) {
379
-		$userNotFound = new \InvalidArgumentException(
380
-			$this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.')
381
-		);
382
-
383
-		$user = $this->userManager->get($input);
384
-		if ($user instanceof IUser) {
385
-			if (!$user->isEnabled()) {
386
-				throw $userNotFound;
387
-			}
388
-
389
-			return $user;
390
-		}
391
-
392
-		$users = \array_filter($this->userManager->getByEmail($input), function (IUser $user) {
393
-			return $user->isEnabled();
394
-		});
395
-
396
-		if (\count($users) === 1) {
397
-			return $users[0];
398
-		}
399
-
400
-		throw $userNotFound;
401
-	}
62
+    /** @var IURLGenerator */
63
+    protected $urlGenerator;
64
+    /** @var IUserManager */
65
+    protected $userManager;
66
+    /** @var Defaults */
67
+    protected $defaults;
68
+    /** @var IL10N */
69
+    protected $l10n;
70
+    /** @var string */
71
+    protected $from;
72
+    /** @var IManager */
73
+    protected $encryptionManager;
74
+    /** @var IConfig */
75
+    protected $config;
76
+    /** @var ISecureRandom */
77
+    protected $secureRandom;
78
+    /** @var IMailer */
79
+    protected $mailer;
80
+    /** @var ITimeFactory */
81
+    protected $timeFactory;
82
+    /** @var ICrypto */
83
+    protected $crypto;
84
+    /** @var ILogger */
85
+    private $logger;
86
+    /** @var Manager */
87
+    private $twoFactorManager;
88
+
89
+    /**
90
+     * @param string $appName
91
+     * @param IRequest $request
92
+     * @param IURLGenerator $urlGenerator
93
+     * @param IUserManager $userManager
94
+     * @param Defaults $defaults
95
+     * @param IL10N $l10n
96
+     * @param IConfig $config
97
+     * @param ISecureRandom $secureRandom
98
+     * @param string $defaultMailAddress
99
+     * @param IManager $encryptionManager
100
+     * @param IMailer $mailer
101
+     * @param ITimeFactory $timeFactory
102
+     * @param ICrypto $crypto
103
+     */
104
+    public function __construct($appName,
105
+                                IRequest $request,
106
+                                IURLGenerator $urlGenerator,
107
+                                IUserManager $userManager,
108
+                                Defaults $defaults,
109
+                                IL10N $l10n,
110
+                                IConfig $config,
111
+                                ISecureRandom $secureRandom,
112
+                                $defaultMailAddress,
113
+                                IManager $encryptionManager,
114
+                                IMailer $mailer,
115
+                                ITimeFactory $timeFactory,
116
+                                ICrypto $crypto,
117
+                                ILogger $logger,
118
+                                Manager $twoFactorManager) {
119
+        parent::__construct($appName, $request);
120
+        $this->urlGenerator = $urlGenerator;
121
+        $this->userManager = $userManager;
122
+        $this->defaults = $defaults;
123
+        $this->l10n = $l10n;
124
+        $this->secureRandom = $secureRandom;
125
+        $this->from = $defaultMailAddress;
126
+        $this->encryptionManager = $encryptionManager;
127
+        $this->config = $config;
128
+        $this->mailer = $mailer;
129
+        $this->timeFactory = $timeFactory;
130
+        $this->crypto = $crypto;
131
+        $this->logger = $logger;
132
+        $this->twoFactorManager = $twoFactorManager;
133
+    }
134
+
135
+    /**
136
+     * Someone wants to reset their password:
137
+     *
138
+     * @PublicPage
139
+     * @NoCSRFRequired
140
+     *
141
+     * @param string $token
142
+     * @param string $userId
143
+     * @return TemplateResponse
144
+     */
145
+    public function resetform($token, $userId) {
146
+        if ($this->config->getSystemValue('lost_password_link', '') !== '') {
147
+            return new TemplateResponse('core', 'error', [
148
+                    'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
149
+                ],
150
+                'guest'
151
+            );
152
+        }
153
+
154
+        try {
155
+            $this->checkPasswordResetToken($token, $userId);
156
+        } catch (\Exception $e) {
157
+            return new TemplateResponse(
158
+                'core', 'error', [
159
+                    "errors" => array(array("error" => $e->getMessage()))
160
+                ],
161
+                'guest'
162
+            );
163
+        }
164
+
165
+        return new TemplateResponse(
166
+            'core',
167
+            'lostpassword/resetpassword',
168
+            array(
169
+                'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
170
+            ),
171
+            'guest'
172
+        );
173
+    }
174
+
175
+    /**
176
+     * @param string $token
177
+     * @param string $userId
178
+     * @throws \Exception
179
+     */
180
+    protected function checkPasswordResetToken($token, $userId) {
181
+        $user = $this->userManager->get($userId);
182
+        if($user === null || !$user->isEnabled()) {
183
+            throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
184
+        }
185
+
186
+        try {
187
+            $encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
188
+            $mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
189
+            $decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
190
+        } catch (\Exception $e) {
191
+            throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
192
+        }
193
+
194
+        $splittedToken = explode(':', $decryptedToken);
195
+        if(count($splittedToken) !== 2) {
196
+            throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
197
+        }
198
+
199
+        if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*24*7) ||
200
+            $user->getLastLogin() > $splittedToken[0]) {
201
+            throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
202
+        }
203
+
204
+        if (!hash_equals($splittedToken[1], $token)) {
205
+            throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
206
+        }
207
+    }
208
+
209
+    /**
210
+     * @param $message
211
+     * @param array $additional
212
+     * @return array
213
+     */
214
+    private function error($message, array $additional=array()) {
215
+        return array_merge(array('status' => 'error', 'msg' => $message), $additional);
216
+    }
217
+
218
+    /**
219
+     * @param array $data
220
+     * @return array
221
+     */
222
+    private function success($data = []) {
223
+        return array_merge($data, ['status'=>'success']);
224
+    }
225
+
226
+    /**
227
+     * @PublicPage
228
+     * @BruteForceProtection(action=passwordResetEmail)
229
+     * @AnonRateThrottle(limit=10, period=300)
230
+     *
231
+     * @param string $user
232
+     * @return JSONResponse
233
+     */
234
+    public function email($user){
235
+        if ($this->config->getSystemValue('lost_password_link', '') !== '') {
236
+            return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
237
+        }
238
+
239
+        \OCP\Util::emitHook(
240
+            '\OCA\Files_Sharing\API\Server2Server',
241
+            'preLoginNameUsedAsUserName',
242
+            ['uid' => &$user]
243
+        );
244
+
245
+        // FIXME: use HTTP error codes
246
+        try {
247
+            $this->sendEmail($user);
248
+        } catch (\Exception $e) {
249
+            // Ignore the error since we do not want to leak this info
250
+            $this->logger->logException($e, [
251
+                'level' => ILogger::WARN
252
+            ]);
253
+        }
254
+
255
+        $response = new JSONResponse($this->success());
256
+        $response->throttle();
257
+        return $response;
258
+    }
259
+
260
+    /**
261
+     * @PublicPage
262
+     * @param string $token
263
+     * @param string $userId
264
+     * @param string $password
265
+     * @param boolean $proceed
266
+     * @return array
267
+     */
268
+    public function setPassword($token, $userId, $password, $proceed) {
269
+        if ($this->config->getSystemValue('lost_password_link', '') !== '') {
270
+            return $this->error($this->l10n->t('Password reset is disabled'));
271
+        }
272
+
273
+        if ($this->encryptionManager->isEnabled() && !$proceed) {
274
+            $encryptionModules = $this->encryptionManager->getEncryptionModules();
275
+            foreach ($encryptionModules as $module) {
276
+                /** @var IEncryptionModule $instance */
277
+                $instance = call_user_func($module['callback']);
278
+                // this way we can find out whether per-user keys are used or a system wide encryption key
279
+                if ($instance->needDetailedAccessList()) {
280
+                    return $this->error('', array('encryption' => true));
281
+                }
282
+            }
283
+        }
284
+
285
+        try {
286
+            $this->checkPasswordResetToken($token, $userId);
287
+            $user = $this->userManager->get($userId);
288
+
289
+            \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
290
+
291
+            if (!$user->setPassword($password)) {
292
+                throw new \Exception();
293
+            }
294
+
295
+            \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
296
+
297
+            $this->twoFactorManager->clearTwoFactorPending($userId);
298
+
299
+            $this->config->deleteUserValue($userId, 'core', 'lostpassword');
300
+            @\OC::$server->getUserSession()->unsetMagicInCookie();
301
+        } catch (HintException $e){
302
+            return $this->error($e->getHint());
303
+        } catch (\Exception $e){
304
+            return $this->error($e->getMessage());
305
+        }
306
+
307
+        return $this->success(['user' => $userId]);
308
+    }
309
+
310
+    /**
311
+     * @param string $input
312
+     * @throws \Exception
313
+     */
314
+    protected function sendEmail($input) {
315
+        $user = $this->findUserByIdOrMail($input);
316
+        $email = $user->getEMailAddress();
317
+
318
+        if (empty($email)) {
319
+            throw new \Exception(
320
+                $this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
321
+            );
322
+        }
323
+
324
+        // Generate the token. It is stored encrypted in the database with the
325
+        // secret being the users' email address appended with the system secret.
326
+        // This makes the token automatically invalidate once the user changes
327
+        // their email address.
328
+        $token = $this->secureRandom->generate(
329
+            21,
330
+            ISecureRandom::CHAR_DIGITS.
331
+            ISecureRandom::CHAR_LOWER.
332
+            ISecureRandom::CHAR_UPPER
333
+        );
334
+        $tokenValue = $this->timeFactory->getTime() .':'. $token;
335
+        $encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
336
+        $this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
337
+
338
+        $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
339
+
340
+        $emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
341
+            'link' => $link,
342
+        ]);
343
+
344
+        $emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
345
+        $emailTemplate->addHeader();
346
+        $emailTemplate->addHeading($this->l10n->t('Password reset'));
347
+
348
+        $emailTemplate->addBodyText(
349
+            htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
350
+            $this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
351
+        );
352
+
353
+        $emailTemplate->addBodyButton(
354
+            htmlspecialchars($this->l10n->t('Reset your password')),
355
+            $link,
356
+            false
357
+        );
358
+        $emailTemplate->addFooter();
359
+
360
+        try {
361
+            $message = $this->mailer->createMessage();
362
+            $message->setTo([$email => $user->getUID()]);
363
+            $message->setFrom([$this->from => $this->defaults->getName()]);
364
+            $message->useTemplate($emailTemplate);
365
+            $this->mailer->send($message);
366
+        } catch (\Exception $e) {
367
+            throw new \Exception($this->l10n->t(
368
+                'Couldn\'t send reset email. Please contact your administrator.'
369
+            ));
370
+        }
371
+    }
372
+
373
+    /**
374
+     * @param string $input
375
+     * @return IUser
376
+     * @throws \InvalidArgumentException
377
+     */
378
+    protected function findUserByIdOrMail($input) {
379
+        $userNotFound = new \InvalidArgumentException(
380
+            $this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.')
381
+        );
382
+
383
+        $user = $this->userManager->get($input);
384
+        if ($user instanceof IUser) {
385
+            if (!$user->isEnabled()) {
386
+                throw $userNotFound;
387
+            }
388
+
389
+            return $user;
390
+        }
391
+
392
+        $users = \array_filter($this->userManager->getByEmail($input), function (IUser $user) {
393
+            return $user->isEnabled();
394
+        });
395
+
396
+        if (\count($users) === 1) {
397
+            return $users[0];
398
+        }
399
+
400
+        throw $userNotFound;
401
+    }
402 402
 }
Please login to merge, or discard this patch.