Completed
Pull Request — master (#7611)
by Blizzz
27:16
created

LostController::error()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
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 \OCP\AppFramework\Controller;
35
use OCP\AppFramework\Http\JSONResponse;
36
use \OCP\AppFramework\Http\TemplateResponse;
37
use OCP\AppFramework\Utility\ITimeFactory;
38
use OCP\Defaults;
39
use OCP\Encryption\IManager;
40
use \OCP\IURLGenerator;
41
use \OCP\IRequest;
42
use \OCP\IL10N;
43
use \OCP\IConfig;
44
use OCP\IUser;
45
use OCP\IUserManager;
46
use OCP\Mail\IMailer;
47
use OCP\Security\ICrypto;
48
use OCP\Security\ISecureRandom;
49
50
/**
51
 * Class LostController
52
 *
53
 * Successfully changing a password will emit the post_passwordReset hook.
54
 *
55
 * @package OC\Core\Controller
56
 */
57
class LostController extends Controller {
58
59
	/** @var IURLGenerator */
60
	protected $urlGenerator;
61
	/** @var IUserManager */
62
	protected $userManager;
63
	/** @var Defaults */
64
	protected $defaults;
65
	/** @var IL10N */
66
	protected $l10n;
67
	/** @var string */
68
	protected $from;
69
	/** @var IManager */
70
	protected $encryptionManager;
71
	/** @var IConfig */
72
	protected $config;
73
	/** @var ISecureRandom */
74
	protected $secureRandom;
75
	/** @var IMailer */
76
	protected $mailer;
77
	/** @var ITimeFactory */
78
	protected $timeFactory;
79
	/** @var ICrypto */
80
	protected $crypto;
81
82
	/**
83
	 * @param string $appName
84
	 * @param IRequest $request
85
	 * @param IURLGenerator $urlGenerator
86
	 * @param IUserManager $userManager
87
	 * @param Defaults $defaults
88
	 * @param IL10N $l10n
89
	 * @param IConfig $config
90
	 * @param ISecureRandom $secureRandom
91
	 * @param string $defaultMailAddress
92
	 * @param IManager $encryptionManager
93
	 * @param IMailer $mailer
94
	 * @param ITimeFactory $timeFactory
95
	 * @param ICrypto $crypto
96
	 */
97 View Code Duplication
	public function __construct($appName,
98
								IRequest $request,
99
								IURLGenerator $urlGenerator,
100
								IUserManager $userManager,
101
								Defaults $defaults,
102
								IL10N $l10n,
103
								IConfig $config,
104
								ISecureRandom $secureRandom,
105
								$defaultMailAddress,
106
								IManager $encryptionManager,
107
								IMailer $mailer,
108
								ITimeFactory $timeFactory,
109
								ICrypto $crypto) {
110
		parent::__construct($appName, $request);
111
		$this->urlGenerator = $urlGenerator;
112
		$this->userManager = $userManager;
113
		$this->defaults = $defaults;
114
		$this->l10n = $l10n;
115
		$this->secureRandom = $secureRandom;
116
		$this->from = $defaultMailAddress;
117
		$this->encryptionManager = $encryptionManager;
118
		$this->config = $config;
119
		$this->mailer = $mailer;
120
		$this->timeFactory = $timeFactory;
121
		$this->crypto = $crypto;
122
	}
123
124
	/**
125
	 * Someone wants to reset their password:
126
	 *
127
	 * @PublicPage
128
	 * @NoCSRFRequired
129
	 *
130
	 * @param string $token
131
	 * @param string $userId
132
	 * @return TemplateResponse
133
	 */
134
	public function resetform($token, $userId) {
135
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
136
			return new TemplateResponse('core', 'error', [
137
					'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
138
				],
139
				'guest'
140
			);
141
		}
142
143
		try {
144
			$this->checkPasswordResetToken($token, $userId);
145
		} catch (\Exception $e) {
146
			return new TemplateResponse(
147
				'core', 'error', [
148
					"errors" => array(array("error" => $e->getMessage()))
149
				],
150
				'guest'
151
			);
152
		}
153
154
		return new TemplateResponse(
155
			'core',
156
			'lostpassword/resetpassword',
157
			array(
158
				'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
159
			),
160
			'guest'
161
		);
162
	}
163
164
	/**
165
	 * @param string $token
166
	 * @param string $userId
167
	 * @throws \Exception
168
	 */
169
	protected function checkPasswordResetToken($token, $userId) {
170
		$user = $this->userManager->get($userId);
171
		if($user === null || !$user->isEnabled()) {
172
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
173
		}
174
175
		try {
176
			$encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
177
			$mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
178
			$decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
179
		} catch (\Exception $e) {
180
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
181
		}
182
183
		$splittedToken = explode(':', $decryptedToken);
184
		if(count($splittedToken) !== 2) {
185
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
186
		}
187
188
		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*12) ||
189
			$user->getLastLogin() > $splittedToken[0]) {
190
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
191
		}
192
193
		if (!hash_equals($splittedToken[1], $token)) {
194
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
195
		}
196
	}
197
198
	/**
199
	 * @param $message
200
	 * @param array $additional
201
	 * @return array
202
	 */
203
	private function error($message, array $additional=array()) {
204
		return array_merge(array('status' => 'error', 'msg' => $message), $additional);
205
	}
206
207
	/**
208
	 * @return array
209
	 */
210
	private function success() {
211
		return array('status'=>'success');
212
	}
213
214
	/**
215
	 * @PublicPage
216
	 * @BruteForceProtection(action=passwordResetEmail)
217
	 * @AnonRateThrottle(limit=10, period=300)
218
	 *
219
	 * @param string $user
220
	 * @return JSONResponse
221
	 */
222
	public function email($user){
223
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
224
			return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
225
		}
226
227
		\OCP\Util::emitHook(
228
			'\OCA\Files_Sharing\API\Server2Server',
229
			'preLoginNameUsedAsUserName',
230
			['uid' => &$user]
231
		);
232
233
		// FIXME: use HTTP error codes
234
		try {
235
			$this->sendEmail($user);
236
		} catch (\Exception $e){
237
			$response = new JSONResponse($this->error($e->getMessage()));
238
			$response->throttle();
239
			return $response;
240
		}
241
242
		$response = new JSONResponse($this->success());
243
		$response->throttle();
244
		return $response;
245
	}
246
247
	/**
248
	 * @PublicPage
249
	 * @param string $token
250
	 * @param string $userId
251
	 * @param string $password
252
	 * @param boolean $proceed
253
	 * @return array
254
	 */
255
	public function setPassword($token, $userId, $password, $proceed) {
256
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
257
			return $this->error($this->l10n->t('Password reset is disabled'));
258
		}
259
260
		if ($this->encryptionManager->isEnabled() && !$proceed) {
261
			return $this->error('', array('encryption' => true));
262
		}
263
264
		try {
265
			$this->checkPasswordResetToken($token, $userId);
266
			$user = $this->userManager->get($userId);
267
268
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
269
270
			if (!$user->setPassword($password)) {
271
				throw new \Exception();
272
			}
273
274
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
275
276
			$this->config->deleteUserValue($userId, 'core', 'lostpassword');
277
			@\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...
278
		} catch (\Exception $e){
279
			return $this->error($e->getMessage());
280
		}
281
282
		return $this->success();
283
	}
284
285
	/**
286
	 * @param string $input
287
	 * @throws \Exception
288
	 */
289
	protected function sendEmail($input) {
290
		$user = $this->findUserByIdOrMail($input);
291
		$email = $user->getEMailAddress();
292
293
		if (empty($email)) {
294
			throw new \Exception(
295
				$this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
296
			);
297
		}
298
299
		// Generate the token. It is stored encrypted in the database with the
300
		// secret being the users' email address appended with the system secret.
301
		// This makes the token automatically invalidate once the user changes
302
		// their email address.
303
		$token = $this->secureRandom->generate(
304
			21,
305
			ISecureRandom::CHAR_DIGITS.
306
			ISecureRandom::CHAR_LOWER.
307
			ISecureRandom::CHAR_UPPER
308
		);
309
		$tokenValue = $this->timeFactory->getTime() .':'. $token;
310
		$encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
311
		$this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
312
313
		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
314
315
		$emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
316
			'link' => $link,
317
		]);
318
319
		$emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
320
		$emailTemplate->addHeader();
321
		$emailTemplate->addHeading($this->l10n->t('Password reset'));
322
323
		$emailTemplate->addBodyText(
324
			$this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.'),
325
			$this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
326
		);
327
328
		$emailTemplate->addBodyButton(
329
			$this->l10n->t('Reset your password'),
330
			$link,
331
			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...
332
		);
333
		$emailTemplate->addFooter();
334
335
		try {
336
			$message = $this->mailer->createMessage();
337
			$message->setTo([$email => $user->getUID()]);
338
			$message->setFrom([$this->from => $this->defaults->getName()]);
339
			$message->useTemplate($emailTemplate);
340
			$this->mailer->send($message);
341
		} catch (\Exception $e) {
342
			throw new \Exception($this->l10n->t(
343
				'Couldn\'t send reset email. Please contact your administrator.'
344
			));
345
		}
346
	}
347
348
	/**
349
	 * @param string $input
350
	 * @return IUser
351
	 * @throws \InvalidArgumentException
352
	 */
353
	protected function findUserByIdOrMail($input) {
354
		$user = $this->userManager->get($input);
355
		if ($user instanceof IUser) {
356
			if (!$user->isEnabled()) {
357
				throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
358
			}
359
360
			return $user;
361
		}
362
		$users = $this->userManager->getByEmail($input);
363
		if (count($users) === 1) {
364
			$user = $users[0];
365
			if (!$user->isEnabled()) {
366
				throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
367
			}
368
369
			return $user;
370
		}
371
372
		throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
373
	}
374
}
375