Completed
Pull Request — master (#32313)
by Sujith
27:06 queued 15:19
created

LostController::sendEmail()   B

Complexity

Conditions 9
Paths 37

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 37
nop 1
dl 0
loc 67
rs 7.1644
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	 * @return null
161
	 * @throws \Exception
162
	 */
163
	private function checkPasswordResetToken(&$token, $userId) {
164
		$user = $this->userManager->get($userId);
165
166
		$splittedToken = \explode(':', $this->config->getUserValue($userId, 'owncloud', 'lostpassword', null));
167
		if (\count($splittedToken) !== 2) {
168
			$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
169
			throw new \Exception($this->l10n->t('Could not reset password because the token is invalid'));
170
		}
171
172
		if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*12) ||
173
			$user->getLastLogin() > $splittedToken[0]) {
174
			if ($user->getLastLogin() !== 0) {
175
				$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
176
				throw new \Exception($this->l10n->t('Could not reset password because the token expired'));
177
			} else {
178
				//This would help to reset the token as the user had not logged in once
179
				$token = $this->secureRandom->generate(21,
180
					ISecureRandom::CHAR_DIGITS,
181
					ISecureRandom::CHAR_LOWER,
0 ignored issues
show
Unused Code introduced by
The call to ISecureRandom::generate() has too many arguments starting with \OCP\Security\ISecureRandom::CHAR_LOWER.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
182
					ISecureRandom::CHAR_UPPER);
183
				$this->config->setUserValue('userId', 'owncloud',
184
					'lostpassword', $this->timeFactory->getTime() . ':' . $token);
185
				return null;
186
			}
187
		}
188
189
		if (!\hash_equals($splittedToken[1], $token)) {
190
			$this->config->deleteUserValue($userId, 'owncloud', 'lostpassword');
191
			throw new \Exception($this->l10n->t('Could not reset password because the token does not match'));
192
		}
193
	}
194
195
	/**
196
	 * @param $message
197
	 * @param array $additional
198
	 * @return array
199
	 */
200
	private function error($message, array $additional= []) {
201
		return \array_merge(['status' => 'error', 'msg' => $message], $additional);
202
	}
203
204
	/**
205
	 * @return array
206
	 */
207
	private function success() {
208
		return ['status'=>'success'];
209
	}
210
211
	/**
212
	 * @PublicPage
213
	 *
214
	 * @param string $user
215
	 * @return array
216
	 */
217
	public function email($user) {
218
		// FIXME: use HTTP error codes
219
		try {
220
			$this->sendEmail($user);
221
		} catch (\Exception $e) {
222
			return $this->error($e->getMessage());
223
		}
224
225
		return $this->success();
226
	}
227
228
	/**
229
	 * @PublicPage
230
	 * @param string $token
231
	 * @param string $userId
232
	 * @param string $password
233
	 * @param boolean $proceed
234
	 * @return array
235
	 */
236
	public function setPassword($token, $userId, $password, $proceed) {
237
		if ($this->isDataEncrypted && !$proceed) {
238
			return $this->error('', ['encryption' => true]);
239
		}
240
241
		try {
242
			$this->checkPasswordResetToken($token, $userId);
243
			$user = $this->userManager->get($userId);
244
245
			if (!$user->setPassword($password)) {
246
				throw new \Exception();
247
			}
248
249
			\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]);
250
			@\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...
251
		} catch (\Exception $e) {
252
			return $this->error($e->getMessage());
253
		}
254
255
		try {
256
			$this->sendNotificationMail($userId);
257
		} catch (\Exception $e) {
258
			return $this->error($e->getMessage());
259
		}
260
261
		$this->logout();
262
263
		return $this->success();
264
	}
265
266
	/**
267
	 * @param string $userId
268
	 * @throws \Exception
269
	 */
270
	protected function sendNotificationMail($userId) {
271
		$user = $this->userManager->get($userId);
272
		$email = $user->getEMailAddress();
273
274
		if ($email !== '') {
275
			$tmpl = new \OC_Template('core', 'lostpassword/notify');
276
			$msg = $tmpl->fetchPage();
277
278
			try {
279
				$message = $this->mailer->createMessage();
280
				$message->setTo([$email => $userId]);
281
				$message->setSubject($this->l10n->t('%s password changed successfully', [$this->defaults->getName()]));
282
				$message->setPlainBody($msg);
0 ignored issues
show
Bug introduced by
It seems like $msg defined by $tmpl->fetchPage() on line 276 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...
283
				$message->setFrom([$this->from => $this->defaults->getName()]);
284
				$this->mailer->send($message);
285
			} catch (\Exception $e) {
286
				throw new \Exception($this->l10n->t(
287
					$e->getMessage()
288
				));
289
			}
290
		}
291
	}
292
293
	/**
294
	 * @param string $user
295
	 * @throws \Exception
296
	 * @return boolean
297
	 */
298
	protected function sendEmail($user) {
299
		if ($this->userManager->userExists($user)) {
300
			$userObject = $this->userManager->get($user);
301
			$email = $userObject->getEMailAddress();
302
303
			if (empty($email)) {
304
				$this->logger->error('Could not send reset email because there is no email address for this username. User: {user}', ['app' => 'core', 'user' => $user]);
305
				return false;
306
			}
307
		} else {
308
			$users = $this->userManager->getByEmail($user);
309
310
			switch (\count($users)) {
311
				case 0:
312
					$this->logger->error('Could not send reset email because User does not exist. User: {user}', ['app' => 'core', 'user' => $user]);
313
					return false;
314
				case 1:
315
					$this->logger->info('User with input as email address found. User: {user}', ['app' => 'core', 'user' => $user]);
316
					$email = $users[0]->getEMailAddress();
317
					$user = $users[0]->getUID();
318
					break;
319
				default:
320
					$this->logger->error('Could not send reset email because the email id is not unique. User: {user}', ['app' => 'core', 'user' => $user]);
321
					return false;
322
			}
323
		}
324
325
		$token = $this->config->getUserValue($user, 'owncloud', 'lostpassword');
326
		if ($token !== '') {
327
			$splittedToken = \explode(':', $token);
328
			if ((\count($splittedToken)) === 2 && $splittedToken[0] > ($this->timeFactory->getTime() - 60 * 5)) {
329
				$this->logger->alert('The email is not sent because a password reset email was sent recently.');
330
				return false;
331
			}
332
		}
333
334
		$token = $this->secureRandom->generate(21,
335
			ISecureRandom::CHAR_DIGITS .
336
			ISecureRandom::CHAR_LOWER .
337
			ISecureRandom::CHAR_UPPER);
338
		$this->config->setUserValue($user, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token);
339
340
		$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user, 'token' => $token]);
341
342
		$tmpl = new \OC_Template('core', 'lostpassword/email');
343
		$tmpl->assign('link', $link);
344
		$msg = $tmpl->fetchPage();
345
		$tmplAlt = new \OC_Template('core', 'lostpassword/altemail');
346
		$tmplAlt->assign('link', $link);
347
		$msgAlt = $tmplAlt->fetchPage();
348
349
		try {
350
			$message = $this->mailer->createMessage();
351
			$message->setTo([$email => $user]);
352
			$message->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
353
			$message->setPlainBody($msgAlt);
0 ignored issues
show
Bug introduced by
It seems like $msgAlt defined by $tmplAlt->fetchPage() on line 347 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...
354
			$message->setHtmlBody($msg);
0 ignored issues
show
Bug introduced by
It seems like $msg defined by $tmpl->fetchPage() on line 344 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...
355
			$message->setFrom([$this->from => $this->defaults->getName()]);
356
			$this->mailer->send($message);
357
		} catch (\Exception $e) {
358
			throw new \Exception($this->l10n->t(
359
				'Couldn\'t send reset email. Please contact your administrator.'
360
			));
361
		}
362
363
		return true;
364
	}
365
366 View Code Duplication
	private function logout() {
367
		$loginToken = $this->request->getCookie('oc_token');
368
		if ($loginToken !== null) {
369
			$this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken);
370
		}
371
		$this->userSession->logout();
372
	}
373
}
374