Completed
Pull Request — master (#32345)
by Sujith
10:55
created

LostController::generateTokenAndLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Bernhard Posselt <[email protected]>
4
 * @author Julius Haertl <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Morris Jobke <[email protected]>
7
 * @author Peter Prochaska <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 * @author Ujjwal Bhardwaj <[email protected]>
10
 * @author Victor Dubiniuk <[email protected]>
11
 *
12
 * @copyright Copyright (c) 2018, ownCloud GmbH
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OC\Core\Controller;
30
31
use \OCP\AppFramework\Controller;
32
use \OCP\AppFramework\Http\TemplateResponse;
33
use OCP\AppFramework\Utility\ITimeFactory;
34
use OCP\ILogger;
35
use \OCP\IURLGenerator;
36
use \OCP\IRequest;
37
use \OCP\IL10N;
38
use \OCP\IConfig;
39
use OCP\IUserManager;
40
use OCP\Mail\IMailer;
41
use OCP\Security\ISecureRandom;
42
use \OC_Defaults;
43
use OC\User\Session;
44
45
/**
46
 * Class LostController
47
 *
48
 * Successfully changing a password will emit the post_passwordReset hook.
49
 *
50
 * @package OC\Core\Controller
51
 */
52
class LostController extends Controller {
53
54
	/** @var IURLGenerator */
55
	protected $urlGenerator;
56
	/** @var IUserManager */
57
	protected $userManager;
58
	// FIXME: Inject a non-static factory of OC_Defaults for better unit-testing
59
	/** @var OC_Defaults */
60
	protected $defaults;
61
	/** @var IL10N */
62
	protected $l10n;
63
	/** @var string */
64
	protected $from;
65
	/** @var bool */
66
	protected $isDataEncrypted;
67
	/** @var IConfig */
68
	protected $config;
69
	/** @var ISecureRandom */
70
	protected $secureRandom;
71
	/** @var IMailer */
72
	protected $mailer;
73
	/** @var ITimeFactory */
74
	protected $timeFactory;
75
	/** @var ILogger */
76
	protected $logger;
77
	/** @var Session */
78
	private $userSession;
79
80
	/**
81
	 * @param string $appName
82
	 * @param IRequest $request
83
	 * @param IURLGenerator $urlGenerator
84
	 * @param IUserManager $userManager
85
	 * @param OC_Defaults $defaults
86
	 * @param IL10N $l10n
87
	 * @param IConfig $config
88
	 * @param ISecureRandom $secureRandom
89
	 * @param string $from
90
	 * @param string $isDataEncrypted
91
	 * @param IMailer $mailer
92
	 * @param ITimeFactory $timeFactory
93
	 * @param ILogger $logger
94
	 * @param Session $userSession
95
	 */
96
	public function __construct($appName,
97
								IRequest $request,
98
								IURLGenerator $urlGenerator,
99
								IUserManager $userManager,
100
								OC_Defaults $defaults,
101
								IL10N $l10n,
102
								IConfig $config,
103
								ISecureRandom $secureRandom,
104
								$from,
105
								$isDataEncrypted,
106
								IMailer $mailer,
107
								ITimeFactory $timeFactory,
108
								ILogger $logger,
109
								Session $userSession) {
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 = $from;
117
		$this->isDataEncrypted = $isDataEncrypted;
0 ignored issues
show
Documentation Bug introduced by
The property $isDataEncrypted was declared of type boolean, but $isDataEncrypted is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
118
		$this->config = $config;
119
		$this->mailer = $mailer;
120
		$this->timeFactory = $timeFactory;
121
		$this->logger = $logger;
122
		$this->userSession = $userSession;
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
		try {
137
			$this->checkPasswordResetToken($token, $userId);
138
		} catch (\Exception $e) {
139
			return new TemplateResponse(
140
				'core', 'error', [
141
					"errors" => [["error" => $e->getMessage()]]
142
				],
143
				'guest'
144
			);
145
		}
146
147
		return new TemplateResponse(
148
			'core',
149
			'lostpassword/resetpassword',
150
			[
151
				'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', ['userId' => $userId, 'token' => $token]),
152
			],
153
			'guest'
154
		);
155
	}
156
157
	/**
158
	 * @param string $token
159
	 * @param string $userId
160
	 * @throws \Exception
161
	 */
162
	private function checkPasswordResetToken($token, $userId) {
163
		$user = $this->userManager->get($userId);
164
165
		$splittedToken = \explode(':', $this->config->getUserValue($userId, 'owncloud', 'lostpassword', null));
166
		if (\count($splittedToken) !== 2) {
167
			$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
168
			throw new \Exception($this->l10n->t('Could not reset password because the token is invalid'));
169
		}
170
171
		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*12) ||
172
			$user->getLastLogin() > $splittedToken[0]) {
173
			$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
174
			throw new \Exception($this->l10n->t('Could not reset password because the token expired'));
175
		}
176
177
		if (!\hash_equals($splittedToken[1], $token)) {
178
			$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
179
			throw new \Exception($this->l10n->t('Could not reset password because the token does not match'));
180
		}
181
	}
182
183
	/**
184
	 * @param $message
185
	 * @param array $additional
186
	 * @return array
187
	 */
188
	private function error($message, array $additional= []) {
189
		return \array_merge(['status' => 'error', 'msg' => $message], $additional);
190
	}
191
192
	/**
193
	 * @return array
194
	 */
195
	private function success() {
196
		return ['status'=>'success'];
197
	}
198
199
	/**
200
	 * @PublicPage
201
	 *
202
	 * @param string $user
203
	 * @return array
204
	 */
205
	public function email($user) {
206
		// FIXME: use HTTP error codes
207
		try {
208
			list($link, $token) = $this->generateTokenAndLink($user);
209
			$this->sendEmail($user, $token, $link);
210
		} catch (\Exception $e) {
211
			return $this->error($e->getMessage());
212
		}
213
214
		return $this->success();
215
	}
216
217
	/**
218
	 * @PublicPage
219
	 * @param string $token
220
	 * @param string $userId
221
	 * @param string $password
222
	 * @param boolean $proceed
223
	 * @return array
224
	 */
225
	public function setPassword($token, $userId, $password, $proceed) {
226
		if ($this->isDataEncrypted && !$proceed) {
227
			return $this->error('', ['encryption' => true]);
228
		}
229
230
		try {
231
			$this->checkPasswordResetToken($token, $userId);
232
			$user = $this->userManager->get($userId);
233
234
			if (!$user->setPassword($password)) {
235
				throw new \Exception();
236
			}
237
238
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]);
239
			@\OC_User::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...
240
		} catch (\Exception $e) {
241
			return $this->error($e->getMessage());
242
		}
243
244
		try {
245
			$this->sendNotificationMail($userId);
246
		} catch (\Exception $e) {
247
			return $this->error($e->getMessage());
248
		}
249
250
		$this->logout();
251
252
		return $this->success();
253
	}
254
255
	/**
256
	 * @param string $userId
257
	 * @throws \Exception
258
	 */
259
	protected function sendNotificationMail($userId) {
260
		$user = $this->userManager->get($userId);
261
		$email = $user->getEMailAddress();
262
263
		if ($email !== '') {
264
			$tmpl = new \OC_Template('core', 'lostpassword/notify');
265
			$msg = $tmpl->fetchPage();
266
267
			try {
268
				$message = $this->mailer->createMessage();
269
				$message->setTo([$email => $userId]);
270
				$message->setSubject($this->l10n->t('%s password changed successfully', [$this->defaults->getName()]));
271
				$message->setPlainBody($msg);
0 ignored issues
show
Bug introduced by
It seems like $msg defined by $tmpl->fetchPage() on line 265 can also be of type boolean; however, OC\Mail\Message::setPlainBody() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
272
				$message->setFrom([$this->from => $this->defaults->getName()]);
273
				$this->mailer->send($message);
274
			} catch (\Exception $e) {
275
				throw new \Exception($this->l10n->t(
276
					$e->getMessage()
277
				));
278
			}
279
		}
280
	}
281
282
	/**
283
	 * @param string $userId
284
	 * @return array
285
	 */
286
	public function generateTokenAndLink($userId) {
287
		$token = $this->secureRandom->generate(21,
288
			ISecureRandom::CHAR_DIGITS .
289
			ISecureRandom::CHAR_LOWER .
290
			ISecureRandom::CHAR_UPPER);
291
292
		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $userId, 'token' => $token]);
293
		return [$link, $token];
294
	}
295
296
	/**
297
	 * @param string $user
298
	 * @param string $token
299
	 * @param string $link
300
	 * @throws \Exception
301
	 * @return boolean
302
	 */
303
	public function sendEmail($user, $token, $link) {
304
		if ($this->userManager->userExists($user)) {
305
			$userObject = $this->userManager->get($user);
306
			$email = $userObject->getEMailAddress();
307
308
			if (empty($email)) {
309
				$this->logger->error('Could not send reset email because there is no email address for this username. User: {user}', ['app' => 'core', 'user' => $user]);
310
				return false;
311
			}
312
		} else {
313
			$users = $this->userManager->getByEmail($user);
314
315
			switch (\count($users)) {
316
				case 0:
317
					$this->logger->error('Could not send reset email because User does not exist. User: {user}', ['app' => 'core', 'user' => $user]);
318
					return false;
319
				case 1:
320
					$this->logger->info('User with input as email address found. User: {user}', ['app' => 'core', 'user' => $user]);
321
					$email = $users[0]->getEMailAddress();
322
					$user = $users[0]->getUID();
323
					break;
324
				default:
325
					$this->logger->error('Could not send reset email because the email id is not unique. User: {user}', ['app' => 'core', 'user' => $user]);
326
					return false;
327
			}
328
		}
329
330
		$getToken = $this->config->getUserValue($user, 'owncloud', 'lostpassword');
331
		if ($getToken !== '') {
332
			$splittedToken = \explode(':', $getToken);
333
			if ((\count($splittedToken)) === 2 && $splittedToken[0] > ($this->timeFactory->getTime() - 60 * 5)) {
334
				$this->logger->alert('The email is not sent because a password reset email was sent recently.');
335
				return false;
336
			}
337
		}
338
339
		$this->config->setUserValue($user, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token);
340
341
		$tmpl = new \OC_Template('core', 'lostpassword/email');
342
		$tmpl->assign('link', $link);
343
		$msg = $tmpl->fetchPage();
344
		$tmplAlt = new \OC_Template('core', 'lostpassword/altemail');
345
		$tmplAlt->assign('link', $link);
346
		$msgAlt = $tmplAlt->fetchPage();
347
348
		try {
349
			$message = $this->mailer->createMessage();
350
			$message->setTo([$email => $user]);
351
			$message->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
352
			$message->setPlainBody($msgAlt);
0 ignored issues
show
Bug introduced by
It seems like $msgAlt defined by $tmplAlt->fetchPage() on line 346 can also be of type boolean; however, OC\Mail\Message::setPlainBody() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
353
			$message->setHtmlBody($msg);
0 ignored issues
show
Bug introduced by
It seems like $msg defined by $tmpl->fetchPage() on line 343 can also be of type boolean; however, OC\Mail\Message::setHtmlBody() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
354
			$message->setFrom([$this->from => $this->defaults->getName()]);
355
			$this->mailer->send($message);
356
		} catch (\Exception $e) {
357
			throw new \Exception($this->l10n->t(
358
				'Couldn\'t send reset email. Please contact your administrator.'
359
			));
360
		}
361
362
		return true;
363
	}
364
365 View Code Duplication
	private function logout() {
366
		$loginToken = $this->request->getCookie('oc_token');
367
		if ($loginToken !== null) {
368
			$this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken);
369
		}
370
		$this->userSession->logout();
371
	}
372
}
373