Completed
Push — master ( 63676d...3faef6 )
by Lukas
28:10 queued 12:28
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 Björn Schießle <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Julius Haertl <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Victor Dubiniuk <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\Core\Controller;
32
33
use \OCP\AppFramework\Controller;
34
use OCP\AppFramework\Http\JSONResponse;
35
use \OCP\AppFramework\Http\TemplateResponse;
36
use OCP\AppFramework\Utility\ITimeFactory;
37
use OCP\Defaults;
38
use OCP\Encryption\IManager;
39
use \OCP\IURLGenerator;
40
use \OCP\IRequest;
41
use \OCP\IL10N;
42
use \OCP\IConfig;
43
use OCP\IUser;
44
use OCP\IUserManager;
45
use OCP\Mail\IMailer;
46
use OCP\Security\ICrypto;
47
use OCP\Security\ISecureRandom;
48
49
/**
50
 * Class LostController
51
 *
52
 * Successfully changing a password will emit the post_passwordReset hook.
53
 *
54
 * @package OC\Core\Controller
55
 */
56
class LostController extends Controller {
57
58
	/** @var IURLGenerator */
59
	protected $urlGenerator;
60
	/** @var IUserManager */
61
	protected $userManager;
62
	/** @var Defaults */
63
	protected $defaults;
64
	/** @var IL10N */
65
	protected $l10n;
66
	/** @var string */
67
	protected $from;
68
	/** @var IManager */
69
	protected $encryptionManager;
70
	/** @var IConfig */
71
	protected $config;
72
	/** @var ISecureRandom */
73
	protected $secureRandom;
74
	/** @var IMailer */
75
	protected $mailer;
76
	/** @var ITimeFactory */
77
	protected $timeFactory;
78
	/** @var ICrypto */
79
	protected $crypto;
80
81
	/**
82
	 * @param string $appName
83
	 * @param IRequest $request
84
	 * @param IURLGenerator $urlGenerator
85
	 * @param IUserManager $userManager
86
	 * @param Defaults $defaults
87
	 * @param IL10N $l10n
88
	 * @param IConfig $config
89
	 * @param ISecureRandom $secureRandom
90
	 * @param string $defaultMailAddress
91
	 * @param IManager $encryptionManager
92
	 * @param IMailer $mailer
93
	 * @param ITimeFactory $timeFactory
94
	 * @param ICrypto $crypto
95
	 */
96
	public function __construct($appName,
97
								IRequest $request,
98
								IURLGenerator $urlGenerator,
99
								IUserManager $userManager,
100
								Defaults $defaults,
101
								IL10N $l10n,
102
								IConfig $config,
103
								ISecureRandom $secureRandom,
104
								$defaultMailAddress,
105
								IManager $encryptionManager,
106
								IMailer $mailer,
107
								ITimeFactory $timeFactory,
108
								ICrypto $crypto) {
109
		parent::__construct($appName, $request);
110
		$this->urlGenerator = $urlGenerator;
111
		$this->userManager = $userManager;
112
		$this->defaults = $defaults;
113
		$this->l10n = $l10n;
114
		$this->secureRandom = $secureRandom;
115
		$this->from = $defaultMailAddress;
116
		$this->encryptionManager = $encryptionManager;
117
		$this->config = $config;
118
		$this->mailer = $mailer;
119
		$this->timeFactory = $timeFactory;
120
		$this->crypto = $crypto;
121
	}
122
123
	/**
124
	 * Someone wants to reset their password:
125
	 *
126
	 * @PublicPage
127
	 * @NoCSRFRequired
128
	 *
129
	 * @param string $token
130
	 * @param string $userId
131
	 * @return TemplateResponse
132
	 */
133
	public function resetform($token, $userId) {
134
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
135
			return new TemplateResponse('core', 'error', [
136
					'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
137
				],
138
				'guest'
139
			);
140
		}
141
142
		try {
143
			$this->checkPasswordResetToken($token, $userId);
144
		} catch (\Exception $e) {
145
			return new TemplateResponse(
146
				'core', 'error', [
147
					"errors" => array(array("error" => $e->getMessage()))
148
				],
149
				'guest'
150
			);
151
		}
152
153
		return new TemplateResponse(
154
			'core',
155
			'lostpassword/resetpassword',
156
			array(
157
				'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
158
			),
159
			'guest'
160
		);
161
	}
162
163
	/**
164
	 * @param string $token
165
	 * @param string $userId
166
	 * @throws \Exception
167
	 */
168
	protected function checkPasswordResetToken($token, $userId) {
169
		$user = $this->userManager->get($userId);
170
		if($user === null) {
171
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
172
		}
173
174
		try {
175
			$encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
176
			$mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
177
			$decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
178
		} catch (\Exception $e) {
179
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
180
		}
181
182
		$splittedToken = explode(':', $decryptedToken);
183
		if(count($splittedToken) !== 2) {
184
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
185
		}
186
187
		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*12) ||
188
			$user->getLastLogin() > $splittedToken[0]) {
189
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
190
		}
191
192
		if (!hash_equals($splittedToken[1], $token)) {
193
			throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
194
		}
195
	}
196
197
	/**
198
	 * @param $message
199
	 * @param array $additional
200
	 * @return array
201
	 */
202
	private function error($message, array $additional=array()) {
203
		return array_merge(array('status' => 'error', 'msg' => $message), $additional);
204
	}
205
206
	/**
207
	 * @return array
208
	 */
209
	private function success() {
210
		return array('status'=>'success');
211
	}
212
213
	/**
214
	 * @PublicPage
215
	 * @BruteForceProtection(action=passwordResetEmail)
216
	 * @AnonRateThrottle(limit=10, period=300)
217
	 *
218
	 * @param string $user
219
	 * @return JSONResponse
220
	 */
221
	public function email($user){
222
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
223
			return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
224
		}
225
226
		// FIXME: use HTTP error codes
227
		try {
228
			$this->sendEmail($user);
229
		} catch (\Exception $e){
230
			$response = new JSONResponse($this->error($e->getMessage()));
231
			$response->throttle();
232
			return $response;
233
		}
234
235
		$response = new JSONResponse($this->success());
236
		$response->throttle();
237
		return $response;
238
	}
239
240
	/**
241
	 * @PublicPage
242
	 * @param string $token
243
	 * @param string $userId
244
	 * @param string $password
245
	 * @param boolean $proceed
246
	 * @return array
247
	 */
248
	public function setPassword($token, $userId, $password, $proceed) {
249
		if ($this->config->getSystemValue('lost_password_link', '') !== '') {
250
			return $this->error($this->l10n->t('Password reset is disabled'));
251
		}
252
253
		if ($this->encryptionManager->isEnabled() && !$proceed) {
254
			return $this->error('', array('encryption' => true));
255
		}
256
257
		try {
258
			$this->checkPasswordResetToken($token, $userId);
259
			$user = $this->userManager->get($userId);
260
261
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
262
263
			if (!$user->setPassword($password)) {
264
				throw new \Exception();
265
			}
266
267
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
268
269
			$this->config->deleteUserValue($userId, 'core', 'lostpassword');
270
			@\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...
271
		} catch (\Exception $e){
272
			return $this->error($e->getMessage());
273
		}
274
275
		return $this->success();
276
	}
277
278
	/**
279
	 * @param string $input
280
	 * @throws \Exception
281
	 */
282
	protected function sendEmail($input) {
283
		$user = $this->findUserByIdOrMail($input);
284
		$email = $user->getEMailAddress();
285
286
		if (empty($email)) {
287
			throw new \Exception(
288
				$this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
289
			);
290
		}
291
292
		// Generate the token. It is stored encrypted in the database with the
293
		// secret being the users' email address appended with the system secret.
294
		// This makes the token automatically invalidate once the user changes
295
		// their email address.
296
		$token = $this->secureRandom->generate(
297
			21,
298
			ISecureRandom::CHAR_DIGITS.
299
			ISecureRandom::CHAR_LOWER.
300
			ISecureRandom::CHAR_UPPER
301
		);
302
		$tokenValue = $this->timeFactory->getTime() .':'. $token;
303
		$encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
304
		$this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
305
306
		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
307
308
		$emailTemplate = $this->mailer->createEMailTemplate();
309
310
		$emailTemplate->addHeader();
311
		$emailTemplate->addHeading($this->l10n->t('Password reset'));
312
313
		$emailTemplate->addBodyText(
314
			$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
			$this->l10n->t('Reset your password'),
320
			$link,
321
			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...
322
		);
323
		$emailTemplate->addFooter();
324
325
		try {
326
			$message = $this->mailer->createMessage();
327
			$message->setTo([$email => $user->getUID()]);
328
			$message->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
329
			$message->setPlainBody($emailTemplate->renderText());
330
			$message->setHtmlBody($emailTemplate->renderHtml());
331
			$message->setFrom([$this->from => $this->defaults->getName()]);
332
			$this->mailer->send($message);
333
		} catch (\Exception $e) {
334
			throw new \Exception($this->l10n->t(
335
				'Couldn\'t send reset email. Please contact your administrator.'
336
			));
337
		}
338
	}
339
340
	/**
341
	 * @param string $input
342
	 * @return IUser
343
	 * @throws \Exception
344
	 */
345
	protected function findUserByIdOrMail($input) {
346
		$user = $this->userManager->get($input);
347
		if ($user instanceof IUser) {
348
			return $user;
349
		}
350
		$users = $this->userManager->getByEmail($input);
351
		if (count($users) === 1) {
352
			return $users[0];
353
		}
354
355
		throw new \InvalidArgumentException($this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.'));
356
	}
357
}
358