Completed
Push — master ( cbe5c1...245d20 )
by Morris
25:18
created

LostController::resetform()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 3
nop 2
dl 0
loc 29
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bernhard Posselt <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Julius Haertl <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Victor Dubiniuk <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OC\Core\Controller;
33
34
use OC\HintException;
35
use \OCP\AppFramework\Controller;
36
use OCP\AppFramework\Http\JSONResponse;
37
use \OCP\AppFramework\Http\TemplateResponse;
38
use OCP\AppFramework\Utility\ITimeFactory;
39
use OCP\Defaults;
40
use OCP\Encryption\IManager;
41
use \OCP\IURLGenerator;
42
use \OCP\IRequest;
43
use \OCP\IL10N;
44
use \OCP\IConfig;
45
use OCP\IUser;
46
use OCP\IUserManager;
47
use OCP\Mail\IMailer;
48
use OCP\Security\ICrypto;
49
use OCP\Security\ISecureRandom;
50
51
/**
52
 * Class LostController
53
 *
54
 * Successfully changing a password will emit the post_passwordReset hook.
55
 *
56
 * @package OC\Core\Controller
57
 */
58
class LostController extends Controller {
59
60
	/** @var IURLGenerator */
61
	protected $urlGenerator;
62
	/** @var IUserManager */
63
	protected $userManager;
64
	/** @var Defaults */
65
	protected $defaults;
66
	/** @var IL10N */
67
	protected $l10n;
68
	/** @var string */
69
	protected $from;
70
	/** @var IManager */
71
	protected $encryptionManager;
72
	/** @var IConfig */
73
	protected $config;
74
	/** @var ISecureRandom */
75
	protected $secureRandom;
76
	/** @var IMailer */
77
	protected $mailer;
78
	/** @var ITimeFactory */
79
	protected $timeFactory;
80
	/** @var ICrypto */
81
	protected $crypto;
82
83
	/**
84
	 * @param string $appName
85
	 * @param IRequest $request
86
	 * @param IURLGenerator $urlGenerator
87
	 * @param IUserManager $userManager
88
	 * @param Defaults $defaults
89
	 * @param IL10N $l10n
90
	 * @param IConfig $config
91
	 * @param ISecureRandom $secureRandom
92
	 * @param string $defaultMailAddress
93
	 * @param IManager $encryptionManager
94
	 * @param IMailer $mailer
95
	 * @param ITimeFactory $timeFactory
96
	 * @param ICrypto $crypto
97
	 */
98 View Code Duplication
	public function __construct($appName,
99
								IRequest $request,
100
								IURLGenerator $urlGenerator,
101
								IUserManager $userManager,
102
								Defaults $defaults,
103
								IL10N $l10n,
104
								IConfig $config,
105
								ISecureRandom $secureRandom,
106
								$defaultMailAddress,
107
								IManager $encryptionManager,
108
								IMailer $mailer,
109
								ITimeFactory $timeFactory,
110
								ICrypto $crypto) {
111
		parent::__construct($appName, $request);
112
		$this->urlGenerator = $urlGenerator;
113
		$this->userManager = $userManager;
114
		$this->defaults = $defaults;
115
		$this->l10n = $l10n;
116
		$this->secureRandom = $secureRandom;
117
		$this->from = $defaultMailAddress;
118
		$this->encryptionManager = $encryptionManager;
119
		$this->config = $config;
120
		$this->mailer = $mailer;
121
		$this->timeFactory = $timeFactory;
122
		$this->crypto = $crypto;
123
	}
124
125
	/**
126
	 * Someone wants to reset their password:
127
	 *
128
	 * @PublicPage
129
	 * @NoCSRFRequired
130
	 *
131
	 * @param string $token
132
	 * @param string $userId
133
	 * @return TemplateResponse
134
	 */
135
	public function resetform($token, $userId) {
136
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
137
			return new TemplateResponse('core', 'error', [
138
					'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
139
				],
140
				'guest'
141
			);
142
		}
143
144
		try {
145
			$this->checkPasswordResetToken($token, $userId);
146
		} catch (\Exception $e) {
147
			return new TemplateResponse(
148
				'core', 'error', [
149
					"errors" => array(array("error" => $e->getMessage()))
150
				],
151
				'guest'
152
			);
153
		}
154
155
		return new TemplateResponse(
156
			'core',
157
			'lostpassword/resetpassword',
158
			array(
159
				'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
160
			),
161
			'guest'
162
		);
163
	}
164
165
	/**
166
	 * @param string $token
167
	 * @param string $userId
168
	 * @throws \Exception
169
	 */
170
	protected function checkPasswordResetToken($token, $userId) {
171
		$user = $this->userManager->get($userId);
172
		if($user === null || !$user->isEnabled()) {
173
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
174
		}
175
176
		try {
177
			$encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
178
			$mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
179
			$decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
180
		} catch (\Exception $e) {
181
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
182
		}
183
184
		$splittedToken = explode(':', $decryptedToken);
185
		if(count($splittedToken) !== 2) {
186
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
187
		}
188
189
		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*12) ||
190
			$user->getLastLogin() > $splittedToken[0]) {
191
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
192
		}
193
194
		if (!hash_equals($splittedToken[1], $token)) {
195
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
196
		}
197
	}
198
199
	/**
200
	 * @param $message
201
	 * @param array $additional
202
	 * @return array
203
	 */
204
	private function error($message, array $additional=array()) {
205
		return array_merge(array('status' => 'error', 'msg' => $message), $additional);
206
	}
207
208
	/**
209
	 * @return array
210
	 */
211
	private function success() {
212
		return array('status'=>'success');
213
	}
214
215
	/**
216
	 * @PublicPage
217
	 * @BruteForceProtection(action=passwordResetEmail)
218
	 * @AnonRateThrottle(limit=10, period=300)
219
	 *
220
	 * @param string $user
221
	 * @return JSONResponse
222
	 */
223
	public function email($user){
224
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
225
			return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
226
		}
227
228
		\OCP\Util::emitHook(
229
			'\OCA\Files_Sharing\API\Server2Server',
230
			'preLoginNameUsedAsUserName',
231
			['uid' => &$user]
232
		);
233
234
		// FIXME: use HTTP error codes
235
		try {
236
			$this->sendEmail($user);
237
		} catch (\Exception $e){
238
			$response = new JSONResponse($this->error($e->getMessage()));
239
			$response->throttle();
240
			return $response;
241
		}
242
243
		$response = new JSONResponse($this->success());
244
		$response->throttle();
245
		return $response;
246
	}
247
248
	/**
249
	 * @PublicPage
250
	 * @param string $token
251
	 * @param string $userId
252
	 * @param string $password
253
	 * @param boolean $proceed
254
	 * @return array
255
	 */
256
	public function setPassword($token, $userId, $password, $proceed) {
257
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
258
			return $this->error($this->l10n->t('Password reset is disabled'));
259
		}
260
261
		if ($this->encryptionManager->isEnabled() && !$proceed) {
262
			return $this->error('', array('encryption' => true));
263
		}
264
265
		try {
266
			$this->checkPasswordResetToken($token, $userId);
267
			$user = $this->userManager->get($userId);
268
269
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
270
271
			if (!$user->setPassword($password)) {
272
				throw new \Exception();
273
			}
274
275
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
276
277
			$this->config->deleteUserValue($userId, 'core', 'lostpassword');
278
			@\OC::$server->getUserSession()->unsetMagicInCookie();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
279
		} catch (HintException $e){
280
			return $this->error($e->getHint());
281
		} catch (\Exception $e){
282
			return $this->error($e->getMessage());
283
		}
284
285
		return $this->success();
286
	}
287
288
	/**
289
	 * @param string $input
290
	 * @throws \Exception
291
	 */
292
	protected function sendEmail($input) {
293
		$user = $this->findUserByIdOrMail($input);
294
		$email = $user->getEMailAddress();
295
296
		if (empty($email)) {
297
			throw new \Exception(
298
				$this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
299
			);
300
		}
301
302
		// Generate the token. It is stored encrypted in the database with the
303
		// secret being the users' email address appended with the system secret.
304
		// This makes the token automatically invalidate once the user changes
305
		// their email address.
306
		$token = $this->secureRandom->generate(
307
			21,
308
			ISecureRandom::CHAR_DIGITS.
309
			ISecureRandom::CHAR_LOWER.
310
			ISecureRandom::CHAR_UPPER
311
		);
312
		$tokenValue = $this->timeFactory->getTime() .':'. $token;
313
		$encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
314
		$this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
315
316
		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
317
318
		$emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
319
			'link' => $link,
320
		]);
321
322
		$emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
323
		$emailTemplate->addHeader();
324
		$emailTemplate->addHeading($this->l10n->t('Password reset'));
325
326
		$emailTemplate->addBodyText(
327
			htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
328
			$this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
329
		);
330
331
		$emailTemplate->addBodyButton(
332
			htmlspecialchars($this->l10n->t('Reset your password')),
333
			$link,
334
			false
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
335
		);
336
		$emailTemplate->addFooter();
337
338
		try {
339
			$message = $this->mailer->createMessage();
340
			$message->setTo([$email => $user->getUID()]);
341
			$message->setFrom([$this->from => $this->defaults->getName()]);
342
			$message->useTemplate($emailTemplate);
343
			$this->mailer->send($message);
344
		} catch (\Exception $e) {
345
			throw new \Exception($this->l10n->t(
346
				'Couldn\'t send reset email. Please contact your administrator.'
347
			));
348
		}
349
	}
350
351
	/**
352
	 * @param string $input
353
	 * @return IUser
354
	 * @throws \InvalidArgumentException
355
	 */
356
	protected function findUserByIdOrMail($input) {
357
		$user = $this->userManager->get($input);
358
		if ($user instanceof IUser) {
359
			if (!$user->isEnabled()) {
360
				throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
361
			}
362
363
			return $user;
364
		}
365
		$users = $this->userManager->getByEmail($input);
366
		if (count($users) === 1) {
367
			$user = $users[0];
368
			if (!$user->isEnabled()) {
369
				throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
370
			}
371
372
			return $user;
373
		}
374
375
		throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
376
	}
377
}
378